#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import logging ; logger = logging . getLogger ( __name__ ) ; logger . info ( " import " )
#Standard Library Modules
from typing import List , Set , Dict , Tuple
#Third Party Modules
from calfbox import cbox
#Template Modules
import template . engine . api #we need direct access to the module to inject data in the provided structures. but we also need the functions directly. next line:
from template . engine . api import *
from template . engine . duration import baseDurationToTraditionalNumber
from template . helper import compress
#Our own engine Modules
pass
DEFAULT_FACTOR = 1 #for the GUI.
#Swing Lookup Table for functions and callbacks
_percentToSwing_Table = { }
_swingToPercent_Table = { }
for value in range ( - 100 , 100 + 1 ) :
#Lookup table.
if value == 0 :
result = 0
elif value > 80 : #81% - 100% is 0.33 - 0.5
result = compress ( value , 81 , 100 , 0.33 , 0.5 )
elif value > 30 : #31% - 80% is 0.15 - 0.33
result = compress ( value , 31 , 80 , 0.15 , 0.32 )
else :
result = compress ( value , 0 , 30 , 0.01 , 0.14 )
r = round ( result , 8 )
_percentToSwing_Table [ value ] = r
_swingToPercent_Table [ r ] = value #TODO: this is risky! it only works because we round to digits and percents are integers.
#New callbacks
class ClientCallbacks ( Callbacks ) : #inherits from the templates api callbacks
def __init__ ( self ) :
super ( ) . __init__ ( )
self . timeSignatureChanged = [ ]
self . scoreChanged = [ ]
self . numberOfMeasuresChanged = [ ]
self . trackStructureChanged = [ ]
self . trackMetaDataChanged = [ ]
self . patternChanged = [ ]
self . stepChanged = [ ]
self . removeStep = [ ]
self . exportCacheChanged = [ ]
self . subdivisionsChanged = [ ]
self . quarterNotesPerMinuteChanged = [ ]
self . loopChanged = [ ]
self . loopMeasureFactorChanged = [ ]
self . patternLengthMultiplicatorChanged = [ ]
self . swingChanged = [ ]
self . swingPercentChanged = [ ]
def _quarterNotesPerMinuteChanged ( self ) :
""" There is one tempo for the entire song in quarter notes per mintue.
score . isTransportMaster to False means we do not create our own changes
and leave everything to the default . Negative values are not possible """
if session . data . tempoMap . isTransportMaster :
export = session . data . tempoMap . getQuarterNotesPerMinute ( )
else :
export = None
for func in self . quarterNotesPerMinuteChanged :
func ( export )
callbacks . _dataChanged ( )
def _setPlaybackTicks ( self ) : #Differs from the template because it has subdivisions.
ppqn = cbox . Transport . status ( ) . pos_ppqn * session . data . subdivisions
status = playbackStatus ( )
for func in self . setPlaybackTicks :
func ( ppqn , status )
def _loopChanged ( self , measurenumber , loopStart , loopEnd ) :
export = measurenumber
for func in self . loopChanged :
func ( export )
def _loopMeasureFactorChanged ( self ) :
""" Very atomic callback. Used only for one value: how many measures are in one loop """
export = session . data . loopMeasureFactor
for func in self . loopMeasureFactorChanged :
func ( export )
def _timeSignatureChanged ( self ) :
nr = session . data . howManyUnits
typ = session . data . whatTypeOfUnit
for func in self . timeSignatureChanged :
func ( nr , typ )
##All patterns and tracks need updates:
for track in session . data . tracks :
self . _patternChanged ( track )
self . _subdivisionsChanged ( ) #update subdivisions. We do not include them in the score or track export on purpose.
callbacks . _dataChanged ( )
def _subdivisionsChanged ( self ) :
""" Subdivisions are tricky, therefore we keep them isolated in their own callback.
You don ' t need to redraw anything if you don ' t want to . One recommendation is to
draw every n step a little more important ( bigger , different color ) .
where n = subdivions
We also place JACK BBT via tempoMap here because we need it every time the time sig
changes ( which calls _subdivisionsChanged ) and if subdivisions change .
"""
typ = baseDurationToTraditionalNumber [ session . data . whatTypeOfUnit ]
nr = session . data . howManyUnits
tradNr = int ( nr / session . data . subdivisions )
#JACK BBT for Timebase Master. No matter if we are master at the moment or not.
if tradNr == nr / session . data . subdivisions :
#Easier to read than the else case. Not possible with 9 Steps per Pattern in Groups of 2 because that is a 4.5/4 timesig.
session . data . tempoMap . setTimeSignature ( tradNr , typ )
else :
#Always possible, compared to first if case.
session . data . tempoMap . setTimeSignature ( nr , typ * session . data . subdivisions )
export = session . data . subdivisions
for func in self . subdivisionsChanged :
func ( export )
callbacks . _dataChanged ( )
def _scoreChanged ( self ) :
""" This includes the time signature as well, but is not send on a timesig change.
A timesig change needs to update all tracks as playback as well as for the GUI
so it is its own callback .
Use this for fast and inexpensive updates like drawing a label or adjusting the
GUI that shows your measure groups ( a label each 8 measures or so ) """
export = session . data . export ( )
for func in self . scoreChanged :
func ( export )
callbacks . _dataChanged ( )
def _exportCacheChanged ( self , track ) :
""" Send the export cache for GUI caching reasons. Don ' t react by redrawing immediately!
This is sent often , redundantly and more than you need .
Example : If you only show one pattern at the same time use this to cache the data
in all hidden patterns and redraw only if you change the active pattern .
You can react to real changes in your active pattern by using patternChanged
and stepChanged . """
export = track . export ( )
for func in self . exportCacheChanged :
func ( export )
def _patternChanged ( self , track ) :
""" each track has only one pattern. We can identify the pattern by track and vice versa.
Don ' t use this to react to clicks on the pattern editor. Use stepChanged instead and
keep book of your incremental updates .
This is used for the whole pattern : timesig changes , invert , clean etc .
"""
export = track . export ( )
self . _exportCacheChanged ( track )
for func in self . patternChanged :
func ( export )
callbacks . _dataChanged ( )
def _stepChanged ( self , track , stepDict ) :
""" A simple GUI will most like not listen to that callback since they
already changed the step on their side . Only useful for parallel
views .
We do not export anything but just sent back the change we received
as dict message . """
self . _exportCacheChanged ( track )
for func in self . stepChanged :
func ( stepDict )
callbacks . _dataChanged ( )
def _removeStep ( self , track , index , pitch ) :
""" Opposite of _stepChanged """
self . _exportCacheChanged ( track )
for func in self . stepChanged :
func ( index , pitch )
callbacks . _dataChanged ( )
def _trackStructureChanged ( self , track ) :
""" update one track structure. Does not export cbox.
Also includes transposition """
export = track . export ( )
for func in self . trackStructureChanged :
func ( export )
callbacks . _dataChanged ( )
def _trackMetaDataChanged ( self , track ) :
""" a low cost function that should not trigger anything costly to redraw
but some text and simple widgets . """
export = track . export ( )
for func in self . trackMetaDataChanged :
func ( export )
callbacks . _dataChanged ( )
def _numberOfMeasuresChanged ( self ) :
export = session . data . export ( )
for func in self . numberOfMeasuresChanged :
func ( export )
callbacks . _dataChanged ( )
def _patternLengthMultiplicatorChanged ( self , track ) :
export = track . export ( )
for func in self . patternLengthMultiplicatorChanged :
func ( export )
self . _patternChanged ( track ) #includes dataChanged
def _swingChanged ( self ) :
export = session . data . swing
for func in self . swingChanged :
func ( export )
for func in self . swingPercentChanged :
func ( _swingToPercent_Table [ export ] )
#Inject our derived Callbacks into the parent module
template . engine . api . callbacks = ClientCallbacks ( )
from template . engine . api import callbacks
_templateStartEngine = startEngine
def updatePlayback ( ) :
#TODO: use template.sequencer.py internal updates instead
cbox . Document . get_song ( ) . update_playback ( )
def startEngine ( nsmClient ) :
_templateStartEngine ( nsmClient )
session . inLoopMode = None # remember if we are in loop mode or not. Positive value is None or a tuple with start and end
#Send initial Callbacks to create the first GUI state.
#The order of initial callbacks must not change to avoid GUI problems.
#For example it is important that the tracks get created first and only then the number of measures
logger . info ( " Sending initial callbacks to GUI " )
callbacks . _numberOfTracksChanged ( )
callbacks . _timeSignatureChanged ( )
callbacks . _numberOfMeasuresChanged ( )
callbacks . _subdivisionsChanged ( )
callbacks . _quarterNotesPerMinuteChanged ( )
callbacks . _loopMeasureFactorChanged ( )
callbacks . _swingChanged ( )
for track in session . data . tracks :
callbacks . _trackMetaDataChanged ( track ) #for colors, scale and simpleNoteNames
callbacks . _patternLengthMultiplicatorChanged ( track ) #for colors, scale and simpleNoteNames
session . data . buildAllTracks ( buildSongDuration = True ) #will set to max track length, we always have a song duration.
updatePlayback ( )
logger . info ( " Patroneo api startEngine complete " )
def _loopOff ( ) :
session . data . buildSongDuration ( ) #no parameter removes the loop
updatePlayback ( )
session . inLoopMode = None
callbacks . _loopChanged ( None , None , None )
def _loopNow ( ) :
now = cbox . Transport . status ( ) . pos_ppqn
_setLoop ( now )
def _setLoop ( loopMeasureAroundPpqn : int ) :
""" This function is used with context.
The loopFactor , how many measures are looped , is saved value """
if loopMeasureAroundPpqn < 0 :
_loopOff ( )
return
loopStart , loopEnd = session . data . buildSongDuration ( loopMeasureAroundPpqn )
session . data . _lastLoopStart = loopStart
updatePlayback ( )
session . inLoopMode = ( loopStart , loopEnd )
assert loopStart < = loopMeasureAroundPpqn < loopEnd
if not playbackStatus ( ) :
cbox . Transport . play ( )
oneMeasureInTicks = ( session . data . howManyUnits * session . data . whatTypeOfUnit ) / session . data . subdivisions
measurenumber , rest = divmod ( loopStart , oneMeasureInTicks )
callbacks . _loopChanged ( int ( measurenumber ) , loopStart , loopEnd )
def setLoopMeasureFactor ( newValue : int ) :
""" How many measures are looped at once. """
if newValue < 1 :
newValue = 1
session . data . loopMeasureFactor = newValue
callbacks . _loopMeasureFactorChanged ( )
if session . inLoopMode :
_setLoop ( session . data . _lastLoopStart )
def toggleLoop ( ) :
""" Plays the current measure as loop.
Current measure is where the playback cursor is
session . inLoopMode is a tuple ( start , end )
"""
if session . inLoopMode :
_loopOff ( )
else :
_loopNow ( )
def rewind ( ) :
""" template.toStart, but removes our loop """
_loopOff ( )
toStart ( )
def seek ( value ) :
""" override template one, which does not have a loop """
if value < 0 :
value = 0
if session . inLoopMode and not session . inLoopMode [ 0 ] < = value < session . inLoopMode [ 1 ] : #if you seek outside the loop the loop will be destroyed.
toggleLoop ( )
cbox . Transport . seek_ppqn ( value )
##Score
def set_quarterNotesPerMinute ( value ) :
if value is None :
session . data . tempoMap . isTransportMaster = False #triggers rebuild
elif value == " on " :
assert not session . data . tempoMap . isTransportMaster
#keep old bpm value. 120 bpm is default.
session . data . tempoMap . isTransportMaster = True #triggers rebuild
else :
assert value > 0
session . data . tempoMap . setQuarterNotesPerMinute ( value )
session . data . tempoMap . isTransportMaster = True #triggers rebuild
#Does not need track rebuilding
updatePlayback ( )
callbacks . _quarterNotesPerMinuteChanged ( )
def set_whatTypeOfUnit ( ticks ) :
""" Denominator of Time Signature """
if session . data . whatTypeOfUnit == ticks : return
session . data . whatTypeOfUnit = ticks
session . data . buildAllTracks ( )
if session . inLoopMode :
_loopNow ( )
updatePlayback ( )
callbacks . _timeSignatureChanged ( )
def set_howManyUnits ( value ) :
""" Numerator of Time Signature """
if session . data . howManyUnits == value : return
session . data . howManyUnits = value
session . data . buildAllTracks ( )
if session . inLoopMode :
_loopNow ( )
updatePlayback ( )
callbacks . _timeSignatureChanged ( )
def set_subdivisions ( value ) :
if session . data . subdivisions == value : return
session . data . subdivisions = value
session . data . buildAllTracks ( )
if session . inLoopMode :
_loopNow ( )
updatePlayback ( )
callbacks . _subdivisionsChanged ( )
def convert_subdivisions ( value , errorHandling ) :
""" " errorHandling can be fail, delete or merge """
if session . data . subdivisions == value : return
result = session . data . convertSubdivisions ( value , errorHandling )
if result :
session . data . buildAllTracks ( )
updatePlayback ( )
callbacks . _timeSignatureChanged ( ) #includes subdivisions
for tr in session . data . tracks :
callbacks . _patternChanged ( tr )
else :
callbacks . _subdivisionsChanged ( ) #to reset the GUI value back to the working one.
if session . inLoopMode :
_loopNow ( )
return result
def set_swing ( value : float ) :
""" A swing that feels natural is not linear. This function sets the absolute value
between - 0.5 and 0.5 but you most likely want to use setSwingPercent which has a non - linear
mapping """
if value < - 0.5 or value > 0.5 :
logger . warning ( f " Swing can only be between -0.5 and 0.5, not { value } " )
return
session . data . swing = value
session . data . buildAllTracks ( )
updatePlayback ( )
callbacks . _swingChanged ( )
def setSwingPercent ( value : int ) :
""" Give value between -100 and 100. 0 is " off " and default.
It will be converted to a number between - 0.5 and 0.5 behind the scenes . This is the value
that gets saved .
Our function will use a lookup - table to convert percentage in a musical way .
The first 80 % will be used for normal musical values . The other 20 for more extreme sounds .
"""
if value < - 100 or value > 100 :
logger . warning ( f " Swing in percent can only be between -100 and +100, not { value } " )
return
set_swing ( _percentToSwing_Table [ value ] )
def set_numberOfMeasures ( value ) :
if session . data . numberOfMeasures == value :
return
session . data . numberOfMeasures = value
session . data . buildSongDuration ( )
updatePlayback ( )
callbacks . _numberOfMeasuresChanged ( )
callbacks . _scoreChanged ( ) #redundant but cheap and convenient
def set_measuresPerGroup ( value ) :
if session . data . measuresPerGroup == value :
return
session . data . measuresPerGroup = value
#No playback change
callbacks . _scoreChanged ( )
def changeTrackName ( trackId , name ) :
track = session . data . trackById ( trackId )
track . sequencerInterface . name = " " . join ( name . split ( ) )
callbacks . _trackMetaDataChanged ( track )
def changeTrackColor ( trackId , colorInHex ) :
""" Expects " #rrggbb """
track = session . data . trackById ( trackId )
assert len ( colorInHex ) == 7 , colorInHex
track . color = colorInHex
callbacks . _trackMetaDataChanged ( track )
def addTrack ( scale = None ) :
if scale :
assert type ( scale ) == tuple
session . data . addTrack ( scale = scale )
callbacks . _numberOfTracksChanged ( )
def createSiblingTrack ( trackId ) :
""" Create a new track with scale, color and jack midi out the same as the given track.
The jack midi out will be independent after creation , but connected to the same instrument
( if any ) """
track = session . data . trackById ( trackId )
assert type ( track . pattern . scale ) == tuple
newTrack = session . data . addTrack ( name = track . sequencerInterface . name , scale = track . pattern . scale , color = track . color , simpleNoteNames = track . pattern . simpleNoteNames ) #track name increments itself from "Track A" to "Track B" or "Track 1" -> "Track 2"
newTrack . pattern . averageVelocity = track . pattern . averageVelocity
newTrack . patternLengthMultiplicator = track . patternLengthMultiplicator
jackConnections = cbox . JackIO . get_connected_ports ( track . sequencerInterface . cboxPortName ( ) )
for port in jackConnections :
cbox . JackIO . port_connect ( newTrack . sequencerInterface . cboxPortName ( ) , port )
#Move new track to neighbour the old one.
oldIndex = session . data . tracks . index ( track )
newIndex = session . data . tracks . index ( newTrack )
newTrackAgain = session . data . tracks . pop ( newIndex )
assert newTrackAgain is newTrack
session . data . tracks . insert ( oldIndex + 1 , newTrackAgain )
callbacks . _numberOfTracksChanged ( )
return newTrack . export ( )
def deleteTrack ( trackId ) :
track = session . data . trackById ( trackId )
session . data . deleteTrack ( track )
if not session . data . tracks : #always keep at least one track
session . data . addTrack ( )
updatePlayback ( )
callbacks . _numberOfTracksChanged ( )
def moveTrack ( trackId , newIndex ) :
""" index is 0 based """
track = session . data . trackById ( trackId )
oldIndex = session . data . tracks . index ( track )
if not oldIndex == newIndex :
session . data . tracks . pop ( oldIndex )
session . data . tracks . insert ( newIndex , track )
callbacks . _numberOfTracksChanged ( )
def setTrackPatternLengthMultiplicator ( trackId , newMultiplicator : int ) :
if newMultiplicator < 1 or not isinstance ( newMultiplicator , int ) :
return #Invalid input
track = session . data . trackById ( trackId )
track . patternLengthMultiplicator = newMultiplicator
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
#Order is important! Otherwise the GUI doesn't know that new empty steps need to exist to fill in.
callbacks . _patternLengthMultiplicatorChanged ( track )
callbacks . _patternChanged ( track )
#Track Switches
def setSwitches ( trackId , setOfPositions , newBool ) :
track = session . data . trackById ( trackId )
if newBool :
track . structure = track . structure . union ( setOfPositions ) #add setOfPositions to the existing one
else :
track . structure = track . structure . difference ( setOfPositions ) #remove everything from setOfPositions that is in the existing one, ignore entries from setOfPositions not in existing.
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackStructureChanged ( track )
def setSwitch ( trackId , position , newBool ) :
track = session . data . trackById ( trackId )
if newBool :
if position in track . structure : return
track . structure . add ( position )
else :
if not position in track . structure : return
track . structure . remove ( position )
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackStructureChanged ( track )
return True
def trackInvertSwitches ( trackId ) :
track = session . data . trackById ( trackId )
"""
if track . structure :
new = set ( i for i in range ( max ( track . structure ) ) )
track . structure = new . difference ( track . structure )
else :
track . structure = set ( i for i in range ( session . data . numberOfMeasures ) )
"""
new = set ( i for i in range ( session . data . numberOfMeasures ) )
track . structure = new . difference ( track . structure )
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackStructureChanged ( track )
def trackOffAllSwitches ( trackId ) :
track = session . data . trackById ( trackId )
track . structure = set ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackStructureChanged ( track )
def trackOnAllSwitches ( trackId ) :
track = session . data . trackById ( trackId )
track . structure = set ( i for i in range ( session . data . numberOfMeasures ) )
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackStructureChanged ( track )
def trackMergeCopyFrom ( sourceTrackId , targetTrackId ) :
if not sourceTrackId == targetTrackId :
sourceTrack = session . data . trackById ( sourceTrackId )
targetTrack = session . data . trackById ( targetTrackId )
targetTrack . structure = targetTrack . structure . union ( sourceTrack . structure )
targetTrack . whichPatternsAreScaleTransposed . update ( sourceTrack . whichPatternsAreScaleTransposed )
targetTrack . whichPatternsAreHalftoneTransposed . update ( sourceTrack . whichPatternsAreHalftoneTransposed )
targetTrack . buildTrack ( )
updatePlayback ( )
callbacks . _trackStructureChanged ( targetTrack )
def trackPatternReplaceFrom ( sourceTrackId , targetTrackId ) :
if not sourceTrackId == targetTrackId :
sourceTrack = session . data . trackById ( sourceTrackId )
targetTrack = session . data . trackById ( targetTrackId )
copyPattern = sourceTrack . pattern . copy ( newParentTrack = targetTrack )
targetTrack . pattern = copyPattern
targetTrack . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( targetTrack )
def setSwitchScaleTranspose ( trackId , position , transpose ) :
""" Scale transposition is flipped. lower value means higher pitch """
track = session . data . trackById ( trackId )
track . whichPatternsAreScaleTransposed [ position ] = transpose
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackStructureChanged ( track )
return True
def setSwitchHalftoneTranspose ( trackId , position , transpose ) :
""" Halftone transposition is not flipped. Higher value means higher pitch """
track = session . data . trackById ( trackId )
track . whichPatternsAreHalftoneTransposed [ position ] = transpose
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackStructureChanged ( track )
return True
def insertSilence ( howMany : int , beforeMeasureNumber : int ) :
""" Insert empty measures into all tracks.
Parameters are un - multiplied . """
#In each track shift every switch to the right if it is before the dividing measure number
#Keep the concept of a "group" even if there are multiplicators in the track.
#If you insert 4 normal measures then a multiplicator-track of two gets only 2 new ones.
for track in session . data . tracks :
thisTrackWhere = beforeMeasureNumber / / track . patternLengthMultiplicator #integer division
thisTrackHowMany = howMany / / track . patternLengthMultiplicator #integer division
track . structure = set ( ( switch + thisTrackHowMany if switch > = thisTrackWhere else switch ) for switch in track . structure )
track . whichPatternsAreScaleTransposed = { ( k + thisTrackHowMany if k > = thisTrackWhere else k ) : v for k , v in track . whichPatternsAreScaleTransposed . items ( ) }
track . whichPatternsAreHalftoneTransposed = { ( k + thisTrackHowMany if k > = thisTrackWhere else k ) : v for k , v in track . whichPatternsAreHalftoneTransposed . items ( ) }
callbacks . _trackStructureChanged ( track )
session . data . buildAllTracks ( )
updatePlayback ( )
def duplicateSwitchGroup ( startMeasureForGroup : int , endMeasureExclusive : int ) :
""" startMeasureForGroup and endMeasureExclusive are in the global, un-multiplied measure counting
format . """
groupSize = endMeasureExclusive - startMeasureForGroup
insertSilence ( groupSize , endMeasureExclusive ) #insert silence handles multiplicator-tracks on its own
for track in session . data . tracks :
thisTrackStartMeasure = startMeasureForGroup / / track . patternLengthMultiplicator #integer division
thisTrackEndMeasure = endMeasureExclusive / / track . patternLengthMultiplicator
thisGroupSize = groupSize / / track . patternLengthMultiplicator
for switch in range ( thisTrackStartMeasure + thisGroupSize , thisTrackEndMeasure + thisGroupSize ) : #One group after the given one.
if switch - thisGroupSize in track . structure :
track . structure . add ( switch )
if switch - thisGroupSize in track . whichPatternsAreScaleTransposed :
track . whichPatternsAreScaleTransposed [ switch ] = track . whichPatternsAreScaleTransposed [ switch - thisGroupSize ]
if switch - thisGroupSize in track . whichPatternsAreHalftoneTransposed :
track . whichPatternsAreHalftoneTransposed [ switch ] = track . whichPatternsAreHalftoneTransposed [ switch - thisGroupSize ]
callbacks . _trackStructureChanged ( track )
session . data . buildAllTracks ( )
updatePlayback ( )
def clearSwitchGroupTranspositions ( startMeasureForGroup : int , endMeasureExclusive : int ) :
""" startMeasureForGroup and endMeasureExclusive are in the global, un-multiplied measure counting
format . """
for track in session . data . tracks :
thisTrackStartMeasure = startMeasureForGroup / / track . patternLengthMultiplicator #integer division
thisTrackEndMeasure = endMeasureExclusive / / track . patternLengthMultiplicator
for switch in range ( thisTrackStartMeasure , thisTrackEndMeasure ) :
if switch in track . whichPatternsAreScaleTransposed :
del track . whichPatternsAreScaleTransposed [ switch ]
if switch in track . whichPatternsAreHalftoneTransposed :
del track . whichPatternsAreHalftoneTransposed [ switch ]
callbacks . _trackStructureChanged ( track )
session . data . buildAllTracks ( )
updatePlayback ( )
def deleteSwitches ( howMany , fromMeasureNumber ) :
""" Parameters are un-multiplied measures. """
for track in session . data . tracks :
thisTrackHowMany = howMany / / track . patternLengthMultiplicator #integer division
thisTrackWhere = fromMeasureNumber / / track . patternLengthMultiplicator #integer division
new_structure = set ( )
for switch in track . structure :
if switch < thisTrackWhere :
new_structure . add ( switch )
elif switch > = thisTrackWhere + thisTrackHowMany : #like a text editor let gravitate left into the hole left by the deleted range
new_structure . add ( switch - thisTrackHowMany )
#else: #discard all in range to delete
track . structure = new_structure
new_scaleTransposed = dict ( )
for k , v in track . whichPatternsAreScaleTransposed . items ( ) :
if k < thisTrackWhere :
new_scaleTransposed [ k ] = v
elif k > = thisTrackWhere + thisTrackHowMany : #like a text editor let gravitate left into the hole left by the deleted range
new_scaleTransposed [ k - thisTrackHowMany ] = v
#else: #discard all in range to delete
track . whichPatternsAreScaleTransposed = new_scaleTransposed
new_halftoneTransposed = dict ( )
for k , v in track . whichPatternsAreHalftoneTransposed . items ( ) :
if k < thisTrackWhere :
new_halftoneTransposed [ k ] = v
elif k > = thisTrackWhere + thisTrackHowMany : #like a text editor let gravitate left into the hole left by the deleted range
new_halftoneTransposed [ k - thisTrackHowMany ] = v
#else: #discard all in range to delete
track . whichPatternsAreHalftoneTransposed = new_halftoneTransposed
callbacks . _trackStructureChanged ( track )
session . data . buildAllTracks ( )
updatePlayback ( )
#Pattern Steps
def setPattern ( trackId , patternList ) :
""" Change the whole pattern, send a callback with the whole pattern """
track = session . data . trackById ( trackId )
track . pattern . data = patternList
track . pattern . buildExportCache ( )
track . buildTrack ( )
callbacks . _patternChanged ( track )
def getAverageVelocity ( trackId ) :
""" If a GUI wants to add a new note and choose a sensible velocity it can use this function """
return session . data . trackById ( trackId ) . pattern . averageVelocity
def setStep ( trackId , stepExportDict ) :
""" This is an atomic operation that only sets one switch and
only sends that switch back via callback . A simple GUI
will most like not listen to that callback since they
already changed the step on their side . Only useful for parallel
views . """
track = session . data . trackById ( trackId )
oldNote = track . pattern . stepByIndexAndPitch ( index = stepExportDict [ " index " ] , pitch = stepExportDict [ " pitch " ] )
if oldNote : #modify existing note
oldNoteIndex = track . pattern . data . index ( oldNote )
track . pattern . data . remove ( oldNote )
track . pattern . data . insert ( oldNoteIndex , stepExportDict ) #for what its worth, insert at the old place. It doesn't really matter though.
else : #new note
track . pattern . data . append ( stepExportDict )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _stepChanged ( track , stepExportDict )
def removeStep ( trackId , index , pitch ) :
""" Reverse of setStep """
track = session . data . trackById ( trackId )
oldNote = track . pattern . stepByIndexAndPitch ( index , pitch )
track . pattern . data . remove ( oldNote )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _removeStep ( track , index , pitch )
def setScale ( trackId , scale , callback = True ) :
""" Expects a scale list or tuple from lowest index to highest.
Actual pitches don ' t matter. " " "
track = session . data . trackById ( trackId )
track . pattern . scale = scale
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
if callback :
callbacks . _trackMetaDataChanged ( track )
def setSimpleNoteNames ( trackId , simpleNoteNames ) :
""" note names is a list of strings with length 128. One name for each midi note.
It is saved to file """
track = session . data . trackById ( trackId )
track . pattern . simpleNoteNames = simpleNoteNames
callbacks . _trackMetaDataChanged ( track )
def transposeHalftoneSteps ( trackId , steps ) :
track = session . data . trackById ( trackId )
track . pattern . scale = [ midipitch + steps for midipitch in track . pattern . scale ]
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackMetaDataChanged ( track )
def patternInvertSteps ( trackId ) :
track = session . data . trackById ( trackId )
track . pattern . invert ( )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
def patternOnAllSteps ( trackId ) :
track = session . data . trackById ( trackId )
track . pattern . fill ( )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
def patternOffAllSteps ( trackId ) :
track = session . data . trackById ( trackId )
track . pattern . empty ( )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
def patternInvertRow ( trackId , pitchindex ) :
""" Pitchindex is the row """
track = session . data . trackById ( trackId )
track . pattern . invertRow ( pitchindex )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
def patternClearRow ( trackId , pitchindex ) :
""" Pitchindex is the row.
Index is the column """
track = session . data . trackById ( trackId )
track . pattern . clearRow ( pitchindex )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
def patternRowRepeatFromStep ( trackId , pitchindex , index ) :
""" Pitchindex is the row.
Index is the column """
track = session . data . trackById ( trackId )
track . pattern . repeatFromStep ( pitchindex , index )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
def patternRowChangeVelocity ( trackId , pitchindex , delta ) :
track = session . data . trackById ( trackId )
for note in track . pattern . getRow ( pitchindex ) :
new = note [ " velocity " ] + delta
note [ " velocity " ] = min ( max ( new , 0 ) , 127 )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
major = [ 0 , 2 , 4 , 5 , 7 , 9 , 11 ] #this is sorted by pitch, lowest to highest. Patroneo works in reverse order to accomodate the row/column approach of a grid. We reverse in setScaleToKeyword
schemesDict = {
#this if sorted by pitch, lowest to highest. Patroneo works in reverse order to accomodate the row/column approach of a grid. We reverse in setScaleToKeyword
#The lowest/first pitch is always 0 because it is just the given root note.
" Major " : [ 0 , 0 , 0 , 0 , 0 , 0 , 0 ] ,
" Minor " : [ 0 , 0 , - 1 , 0 , 0 , - 1 , - 1 ] ,
" Dorian " : [ 0 , 0 , - 1 , 0 , 0 , 0 , - 1 ] ,
" Phrygian " : [ 0 , - 1 , - 1 , 0 , 0 , - 1 , - 1 ] ,
" Lydian " : [ 0 , 0 , 0 , + 1 , 0 , 0 , 0 ] ,
" Mixolydian " : [ 0 , 0 , 0 , 0 , 0 , 0 , - 1 ] ,
" Locrian " : [ 0 , - 1 , - 1 , 0 , - 1 , - 1 , - 1 ] ,
#"Blues": [0,-2,-1,0,-1,-2,-1], #blues is a special case. It has less notes than we offer.
" Blues " : [ 0 , + 1 , + 1 , + 1 , 0 , + 2 , + 1 ] , #broden. Needs double octave in the middle. better than completely wrong.
" Hollywood " : [ 0 , 0 , 0 , 0 , 0 , - 1 , - 1 ] , #The "Hollywood"-Scale. Stargate, Lord of the Rings etc.
" Chromatic " : [ 0 , - 1 , - 2 , - 2 , - 3 , - 4 , - 5 , - 5 , - 6 , - 7 , - 7 , - 8 , - 9 , - 10 , - 10 ] , #crude... also broken > 2 octaves
}
major . reverse ( )
for l in schemesDict . values ( ) :
l . reverse ( )
#Ordered version
schemes = [
" Major " ,
" Minor " ,
" Dorian " ,
" Phrygian " ,
" Lydian " ,
" Mixolydian " ,
" Locrian " ,
" Blues " ,
" Hollywood " ,
" Chromatic " ,
]
def setScaleToKeyword ( trackId , keyword ) :
""" Use a builtin base scale and apply to all notes in a pattern. If there are more more ore
fewer notes in the pattern than in the scale we will calculate the rest .
This function is called not often and does not need to be performant .
"""
track = session . data . trackById ( trackId )
rememberRootNote = track . pattern . scale [ - 1 ] #The last note has a special role by convention. No matter if this is the lowest midi-pitch or not. Most of the time it is the lowest though.
#Create a modified scalePattern for the tracks numberOfSteps.
#We technically only need to worry about creating additional steps. less steps is covered by zip(), see below
majorExt = [ ]
schemeExt = [ ]
mrev = list ( reversed ( major * 16 ) ) #pad major to the maximum possible notes. We just need the basis to make it possible long schemes like chromatic fit
srev = list ( reversed ( schemesDict [ keyword ] * 16 ) )
for i in range ( track . pattern . numberOfSteps ) :
l = len ( srev )
octaveOffset = i / / l * 12 #starts with 0*12
majorExt . append ( mrev [ i % l ] + octaveOffset )
schemeExt . append ( srev [ i % l ] ) #this is always the same. it is only the difference to the major scale
majorExt = list ( reversed ( majorExt ) )
schemeExt = list ( reversed ( schemeExt ) )
scale = [ x + y for x , y in zip ( majorExt , schemeExt ) ] #zip just creates pairs until it reached the end of one of its arguments. This is reversed order.
#scale = [x + y for x, y in zip(major, schemesDict[keyword])] #zip just creates pairs until it reached the end of one of its arguments. This is reversed order.
difference = rememberRootNote - scale [ - 1 ] #isn't this the same as rootnote since scale[-1] is always 0? Well, we could have hypo-scales in the future.
result = [ midipitch + difference for midipitch in scale ] #create actual midi pitches from the root note and the scale. This is reversed order because "scale" is.
#Here is a hack because chromatic didn't work with octave wrap-around. We want to make sure we don't fall back to a lower octave
r = reversed ( result )
result = [ ]
oldPitch = 0
for p in r :
while p < oldPitch :
p + = 12
result . append ( p )
oldPitch = p
result = reversed ( result )
#Done. Inform all parties.
track . pattern . scale = result
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _trackMetaDataChanged ( track )
def changePatternVelocity ( trackId , steps ) :
track = session . data . trackById ( trackId )
for note in track . pattern . data :
new = note [ " velocity " ] + steps
note [ " velocity " ] = min ( max ( new , 0 ) , 127 )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
def resizePatternWithoutScale ( trackId , steps ) :
""" Resize a patterns number of steps without changing the scale.
Can ' t go below 1 step.
Our editing end is the bottom one , where new steps are removed or added .
We also cannot know if there the user set a scale through an api scheme . At the very least
this needs analyzing and taking an educated guess . For now we just add notes a semitone below .
"""
if steps < 1 or steps > 128 :
logger . warning ( f " Pattern must have >= 1 and <= 127 steps but { steps } was requested. Doing nothing. " )
return
track = session . data . trackById ( trackId )
currentNr = track . pattern . numberOfSteps
oldid = id ( track . pattern . scale )
s = track . pattern . scale #GUI view: from top to bottom. Usually from higher pitches to lower. (49, 53, 50, 45, 42, 39, 38, 36)
if steps == currentNr :
return
if steps < currentNr : #just reduce
track . pattern . scale = tuple ( s [ : steps ] )
else : #new
currentLowest = s [ - 1 ] #int
result = list ( s ) #can be edited.
for i in range ( steps - currentNr ) :
currentLowest - = 1
result . append ( currentLowest )
track . pattern . scale = tuple ( result )
assert track . pattern . numberOfSteps == steps , ( track . pattern . numberOfSteps , steps )
assert not oldid == id ( track . pattern . scale )
track . pattern . buildExportCache ( )
track . buildTrack ( )
updatePlayback ( )
callbacks . _patternChanged ( track )
#Other functions. These can't be template functions because they use a specific track and Patroneos row and scale system.
def noteOn ( trackId , row ) :
track = session . data . trackById ( trackId )
midipitch = track . pattern . scale [ row ]
cbox . send_midi_event ( 0x90 , midipitch , track . pattern . averageVelocity , output = track . sequencerInterface . cboxMidiOutUuid )
def noteOff ( trackId , row ) :
track = session . data . trackById ( trackId )
midipitch = track . pattern . scale [ row ]
cbox . send_midi_event ( 0x80 , midipitch , track . pattern . averageVelocity , output = track . sequencerInterface . cboxMidiOutUuid )