importtemplate.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:
fromtemplate.engine.apiimport*
DEFAULT_VELOCITY=90
DEFAULT_FACTOR=1
NUMBER_OF_STEPS=8#for exchange with the GUI: one octave of range per pattern. This is unlikely to change and then will not work immediately, but better to keep it as a named "constant".
#New callbacks
classClientCallbacks(Callbacks):#inherits from the templates api callbacks
@ -27,15 +30,15 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
"""There is one tempo for the entire song in quarter notes per mintue.
newTrack=session.score.addTrack(name=track.name,scale=track.pattern.scale,color=track.color,noteNames=track.pattern.noteNames)#track name increments itself from "Track A" to "Track B" or "Track 1" -> "Track 2"
newTrack=session.data.addTrack(name=track.name,scale=track.pattern.scale,color=track.color,noteNames=track.pattern.noteNames)#track name increments itself from "Track A" to "Track B" or "Track 1" -> "Track 2"
ifnotsession.score.tracks:#always keep at least one track
session.score.addTrack()
track=session.data.trackById(trackId)
session.data.deleteTrack(track)
ifnotsession.data.tracks:#always keep at least one track
session.data.addTrack()
_updatePlayback()
callbacksDatabase._numberOfTracksChanged()
callbacks._numberOfTracksChanged()
defmoveTrack(trackId,newIndex):
"""index is 0 based"""
track=session.score.trackById(trackId)
oldIndex=session.score.tracks.index(track)
track=session.data.trackById(trackId)
oldIndex=session.data.tracks.index(track)
ifnotoldIndex==newIndex:
session.score.tracks.pop(oldIndex)
session.score.tracks.insert(newIndex,track)
callbacksDatabase._numberOfTracksChanged()
session.data.tracks.pop(oldIndex)
session.data.tracks.insert(newIndex,track)
callbacks._numberOfTracksChanged()
#Track Switches
defsetSwitches(trackId,setOfPositions,newBool):
track=session.score.trackById(trackId)
track=session.data.trackById(trackId)
ifnewBool:
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.
major=[0,2,4,5,7,9,11,12]#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
@ -569,7 +574,7 @@ schemes = [
]
defsetScaleToKeyword(trackId,keyword):
track=session.score.trackById(trackId)
track=session.data.trackById(trackId)
rememberRootNote=track.pattern.scale[-1]#no matter if this is the lowest or not%
self.measuresPerGroup=measuresPerGroup# meta data, has no effect on playback.
self.subdivisions=subdivisions
self.lastUsedNotenames=noteNames["English"]#The default value for new tracks/patterns. Changed each time the user picks a new representation via api.setNoteNames . noteNames are saved with the patterns.
ifnottracks:#Empty / New project
self.tracks=[]
self.addTrack(name="Melody A",color="#ffff00")
self.tracks[0].structure=set((0,))#Already have the first pattern activated, so 'play' after startup already produces sounding notes. This is less confusing for a new user.
self.addTrack(name="Bass A",color="#00ff00")
self.addTrack(name="Drums A",color="#ff5500")
deftrackById(self,trackId):
fortrackinself.tracks:
iftrackId==id(track):
returntrack
raiseValueError(f"Track {trackId} not found. Current Tracks: {[id(tr)fortrinself.tracks]}")
else:# if error handling was "merge" then impossible conversions will lead to step positions that can't be undone by restoring the old subdivision value.
step["index"]=int(scaleFactor*step["index"])#yes, not inverse.
cbox.Document.get_song().set_loop(maxTrackDuration,maxTrackDuration)#set playback length for the entire score. Why is the first value not zero? That would create an actual loop from the start to end. We want the song to play only once. The cbox way of doing that is to set the loop range to zero at the end of the track. Zero length is stop.
else:
loopMeasure=int(loopMeasureAroundPpqn/oneMeasureInTicks)#0 based
start=loopMeasure*oneMeasureInTicks
end=start+oneMeasureInTicks
cbox.Document.get_song().set_loop(start,end)#set playback length for the entire score. Why is the first value not zero? That would create an actual loop from the start to end. We want the song to play only once. The cbox way of doing that is to set the loop range to zero at the end of the track. Zero length is stop.
self.structure=structureifstructureelseset()#see buildTrack(). This is the main track data structure besides the pattern. Just integers (starts at 0) as switches which are positions where to play the patterns. In between are automatic rests.
ifwhichPatternsAreScaleTransposed:
self.whichPatternsAreScaleTransposed={int(k):int(v)fork,vinwhichPatternsAreScaleTransposed.items()}#json saves dict keys as strings
else:
self.whichPatternsAreScaleTransposed={}#position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
ifwhichPatternsAreHalftoneTransposed:
self.whichPatternsAreHalftoneTransposed={int(k):int(v)fork,vinwhichPatternsAreHalftoneTransposed.items()}#json saves dict keys as strings
else:
self.whichPatternsAreHalftoneTransposed={}#position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
defbuildTrack(self):
"""The goal is to create a cbox-track, consisting of cbox-clips which hold cbox-pattern,
oneMeasureInTicks=(self.parentScore.howManyUnits*self.parentScore.whatTypeOfUnit)/self.parentScore.subdivisions#subdivisions is 1 by default. bigger values mean shorter values, which is compensated by the user setting bigger howManyUnits manually.
oneMeasureInTicks=int(oneMeasureInTicks)
filteredStructure=[indexforindexinsorted(self.structure)ifindex<self.parentScore.numberOfMeasures]#not <= because we compare count with range