#! /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 ) ,
Laborejo2 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 ; logger = logging . getLogger ( __name__ ) ; logger . info ( " import " )
#Python Standard Library
import sys
import random
random . seed ( )
from typing import Iterable , Callable , Tuple
#Template Modules
from template . calfbox import cbox
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 DM , DB , DL , D1 , D2 , D4 , D8 , D16 , D32 , D64 , D128 , D256 , D512 , D1024 , D_DEFAULT , D_STACCATO , D_TENUTO , D_TIE
import template . engine . pitch as pitchmath
from template . helper import flatList , EndlessGenerator
#Our Modules
from . import items
from . import lilypond
from . tempotrack import TempoItem , TempoTrack
from . ccsubtrack import GraphItem , GraphTrackCC
from . track import Track
apiModuleSelfReference = sys . modules [ __name__ ]
#New callbacks
class ClientCallbacks ( Callbacks ) : #inherits from the templates api callbacks
def __init__ ( self ) :
super ( ) . __init__ ( )
self . setCursor = [ ]
self . setSelection = [ ]
self . tracksChanged = [ ]
self . tracksChangedIncludingHidden = [ ]
self . updateTrack = [ ]
self . updateBlockTrack = [ ]
#self.playbackStart = []
#self.playbackStop = []
self . _cachedTickIndex = - 1
self . updateGraphTrackCC = [ ]
self . graphCCTracksChanged = [ ]
self . updateGraphBlockTrack = [ ]
self . updateTempoTrack = [ ]
self . updateTempoTrackBlocks = [ ]
self . updateTempoTrackMeta = [ ]
self . tempoScalingChanged = [ ]
self . prevailingBaseDurationChanged = [ ]
#self.recordingStreamNoteOn = []
#self.recordingStreamNoteOff = []
#self.recordingStreamClear = []
def _dataChanged ( self ) :
""" Only called from within the callbacks.
This is about data the user cares about . In other words this is the indicator if you need
to save again .
The data changed when notes are inserted , track names are changed , a CC value is added etc .
but not when the cursor moves , the metronome is generated or the playback tick advances .
"""
session . nsmClient . announceSaveStatus ( False )
self . _historyChanged ( )
def _setCursor ( self , destroySelection = True ) :
""" set a new cursor position.
Only do destroySelection = False if you really modify the
selection . If in doubt : destroy it . """
if destroySelection or session . history . doNotRegisterRightNow or session . history . duringRedo :
session . data . cursorWhenSelectionStarted = None
if self . setCursor :
ex = session . data . cursorExport ( )
for func in self . setCursor :
func ( ex )
""" Exports a tuple of cursors. This is to indicate a GUI to
draw a selection rectangle or so . Not for processing . Because :
The exported top left item is included in the selection .
The exported bottom right item is NOT in the selection .
But for a GUI it looks like the rectangle goes to the current
cursor position , which appears to be left of the current item . """
ex = session . data . selectionExport ( )
for func in self . setSelection :
func ( ex )
def _updateChangedTracks ( self ) :
""" Determines which tracks have changed in the step before
this callback and calls an update for those """
changedTracks , currentItem = session . data . currentContentLinkAndItsBlocksInAllTracks ( )
for track in changedTracks :
self . _updateTrack ( id ( track ) )
if session . data . currentMetronomeTrack is track :
setMetronome ( track . asMetronomeData , label = track . name ) #template api
def _updateTrack ( self , trId ) :
""" The most important function. Create a static representation of the music data which
can be used by a GUI to draw notes .
track . staticRepresentation also creates the internal midi representation for cbox . """
if self . updateTrack :
ex = session . data . trackById ( trId ) . staticRepresentation ( )
for func in self . updateTrack :
func ( trId , ex )
self . _updateBlockTrack ( trId )
def _updateBlockTrack ( self , trId ) :
if self . updateBlockTrack :
ex = session . data . trackById ( trId ) . staticBlocksRepresentation ( )
for func in self . updateBlockTrack :
func ( trId , ex )
self . _updateTempoTrack ( ) #The last Tempo Block reacts to changing score sizes.
self . _dataChanged ( )
def _updateGraphTrackCC ( self , trId , cc ) :
""" No cursor, so it always needs a graphTrack Id.
Also handles a GraphBlock overview callback ,
for extra block view or background colors . """
track = session . data . trackById ( trId )
if cc in track . ccGraphTracks :
graphTracksThatNeedUpdate = track . ccGraphTracks [ cc ] . otherTracksWithOurLinkedContent ( )
for gTr in graphTracksThatNeedUpdate :
trId = id ( gTr . parentTrack )
ex = gTr . staticRepresentation ( )
for func in self . updateGraphTrackCC :
func ( trId , cc , ex )
ex = gTr . staticGraphBlocksRepresentation ( )
for func in self . updateGraphBlockTrack :
func ( trId , cc , ex )
def _updateSingleTrackAllCC ( self , trId ) :
""" Used on CC-Channels change. No own callback list. """
for cc , graphTrackCC in session . data . trackById ( trId ) . ccGraphTracks . items ( ) :
ex = graphTrackCC . staticRepresentation ( )
for func in self . updateGraphTrackCC :
func ( trId , cc , ex )
def _graphCCTracksChanged ( self , trId ) :
""" CC graphs of a track deleted or added.
Does not need to react to block or item changes , even ( not ) block duration changes
which change the tracks overall duration ! """
#TODO: moved?
if self . graphCCTracksChanged :
ex = list ( session . data . trackById ( trId ) . ccGraphTracks . keys ( ) ) # list of ints from 0 to 127
for func in self . graphCCTracksChanged :
func ( trId , ex )
self . _dataChanged ( )
def _tracksChanged ( self ) :
""" Track deleted, added or moved. Or toggled double/non-double, visible, eudible etc.
This callback is relatively cheap because it does not generate
any item data . """
#TODO: does NOT call template.api._numberOfTracksChanged
session . data . updateJackMetadataSorting ( )
ex = session . data . listOfStaticTrackRepresentations ( )
if self . tracksChanged :
for func in self . tracksChanged :
func ( ex )
if self . tracksChangedIncludingHidden :
exHidden = session . data . listOfStaticHiddenTrackRepresentations ( )
for func in reversed ( self . tracksChangedIncludingHidden ) :
func ( ex + exHidden )
self . _dataChanged ( )
session . data . metronome . label = session . data . currentMetronomeTrack . name #not the expensive redundant data generation
self . _metronomeChanged ( ) #because the track name is sent here.
def _updateTempoTrack ( self ) :
""" Sends the block update as well.
staticRepresentations also updates midi .
Of course the order is : track meta , then blocks first , then items .
This must not change ! The GUI depends on it . """
if self . updateTempoTrackMeta :
ex = session . data . tempoTrack . staticTrackRepresentation ( ) #only track metadata, no block information. parses all items.
for func in self . updateTempoTrackMeta :
func ( ex )
if self . updateTempoTrackBlocks :
ex = session . data . tempoTrack . staticGraphBlocksRepresentation ( ) #block boundaries and meta data. Does not parse items therefore cheap to call.
for func in self . updateTempoTrackBlocks :
func ( ex )
if self . updateTempoTrack :
ex = session . data . tempoTrack . staticRepresentation ( ) #export all items. performance-expensive.
for func in self . updateTempoTrack :
func ( ex )
#TODO: We need blocks before items, but also blocks after items. This is a cheap call though.
if self . updateTempoTrackBlocks :
ex = session . data . tempoTrack . staticGraphBlocksRepresentation ( ) #yes, we need to regenerate that. That is the problem. #TODO.
for func in self . updateTempoTrackBlocks :
func ( ex )
self . _dataChanged ( )
#since the exported cursor also has a tempo entry it needs updating as well
#TODO: but this also leads to centerOn cursor in the gui after every mouse click in the conductor.
#self._setCursor(destroySelection = False)
def _tempoScalingChanged ( self , newValue ) :
for func in self . tempoScalingChanged :
func ( newValue )
def _playbackStart ( self ) :
for func in self . playbackStart :
func ( )
def _playbackStop ( self ) :
for func in self . playbackStop :
func ( )
def _prevailingBaseDurationChanged ( self , newPrevailingBaseDuration ) :
for func in self . prevailingBaseDurationChanged :
func ( newPrevailingBaseDuration )
def _historyChanged ( self ) :
""" sends two lists of strings.
the first is the undoHistory , the last added item is [ - 1 ] . We can show that to a user to
indicate what the next undo will do .
the second is redoHistory , same as undo : [ - 1 ] shows the next redo action . """
undoHistory , redoHistory = session . history . asList ( )
for func in self . historyChanged :
func ( undoHistory , redoHistory )
def _recordingStreamNoteOn ( self , liveChord ) :
""" One dict at a time """
trId = id ( session . data . currentTrack ( ) )
for func in self . recordingStreamNoteOn :
func ( trId , liveChord )
def _recordingStreamNoteOff ( self , liveChord ) :
""" One dict at a time """
trId = id ( session . data . currentTrack ( ) )
for func in self . recordingStreamNoteOff :
func ( trId , liveChord )
def _recordingStreamClear ( self ) :
for func in self . recordingStreamClear :
func ( )
#Inject our derived Callbacks into the parent module
template . engine . api . callbacks = ClientCallbacks ( )
from template . engine . api import callbacks
_templateStartEngine = startEngine
def startEngine ( nsmClient , additionalData : dict = { } ) :
_templateStartEngine ( nsmClient )
#Send initial Data etc.
session . data . tempoMap . isTransportMaster = True #always true for Laborejo.
callbacks . _tracksChanged ( ) # This creates the frontend/GUI tracks with access through track ids. From now on we can send the GUI a trId and it knows which track needs change.
for track in session . data . hiddenTracks : #After loading a file some tracks could be hidden. We want midi for them as well.
track . staticRepresentation ( ) #that generates the calfbox data as a side effect. discard the other data.
#because we do a simplicifaction in _updateGraphTrackCC that calls all tracks for one CC (for content links) we need to build the structure first, only during file load.
for trId in session . data . listOfTrackIds ( ) :
callbacks . _graphCCTracksChanged ( trId ) #create structure: all CC graphs, accessed by CC number (0-127)
for trId in session . data . listOfTrackIds ( ) :
callbacks . _updateTrack ( trId ) #create content: music items
callbacks . _updateTempoTrack ( ) # does everything at once
#done above. Special situation at file load. callbacks._graphCCTracksChanged(trId) #create structure: all CC graphs, accessed by CC number (0-127)
for cc in session . data . trackById ( trId ) . listOfCCsInThisTrack ( ) :
callbacks . _updateGraphTrackCC ( trId , cc ) #create content: CC points. user points and interpolated points.
if session . data . currentMetronomeTrack . asMetronomeData :
setMetronome ( session . data . currentMetronomeTrack . asMetronomeData , label = session . data . currentMetronomeTrack . name ) #track.asMetronomeData is generated in staticRepresentation #template api. has callbacks
callbacks . _setCursor ( )
if session . data . metaData [ " title " ] :
cbox . JackIO . Metadata . client_set_property ( " http://jackaudio.org/metadata/pretty-name " , session . data . metaData [ " title " ] )
global laborejoEngineStarted #makes for a convenient check. stepMidiInput uses it, which needs to know that the gui already started the api.
laborejoEngineStarted = True
#General and abstract Commands
def getMetadata ( ) :
""" Do not confuse with template/config METADATA. This is Lilypond title, composer etc. """
return session . data . metaData
def setMetadata ( data ) :
titleBefore = bool ( session . data . metaData [ " title " ] )
session . data . metaData = data
if session . data . metaData [ " title " ] :
cbox . JackIO . Metadata . client_set_property ( " http://jackaudio.org/metadata/pretty-name " , session . data . metaData [ " title " ] )
elif titleBefore :
cbox . JackIO . Metadata . client_remove_property ( " http://jackaudio.org/metadata/pretty-name " )
def playFromCursor ( ) :
playFrom ( ticks = session . data . cursorExport ( ) [ " tickindex " ] )
def playFromBlockStart ( ) :
tr = session . data . currentTrack ( )
ticks = 0
for idx , bl in enumerate ( tr . blocks ) :
if idx == tr . state . blockindex :
playFrom ( ticks )
return
else :
ticks + = bl . duration ( )
else :
raise RuntimeError ( " reached end of blocks without matchin current block index " )
def getMidiInputNameAndUuid ( ) - > ( str , int ) : #tuple name:str, uuid
""" Override template function. We access the stepMidi directly.
Used by the quick midi input widget
Return name and cboxMidiPortUid .
name is Client : Port JACK format
If not return None , None
"""
from engine . midiinput . stepmidiinput import stepMidiInput #singleton instance #avoid circular dependency. stepMidiInput import api
if stepMidiInput . ready : #startup delay
return stepMidiInput . fullName ( ) , stepMidiInput . cboxMidiPortUid
else :
return None , None
topLevelFunction = None
def simpleCommand ( function , autoStepLeft = True , forceReturnToItem = None ) :
"""
simpleCommand demands that the cursor is on the item you want to
change . That means for scripting and working with selections
that you can ' t simply iterate over block.data, which is a list.
Well you can , but then you don ' t get the context data which is in
track . state .
Recursive commands are not allowed """
global topLevelFunction #todo: replace with a python-context
if not topLevelFunction :
topLevelFunction = function
if autoStepLeft and session . data . currentTrack ( ) . state . isAppending ( ) :
session . data . currentTrack ( ) . left ( )
function ( ) #<-------- The action
session . data . currentTrack ( ) . right ( )
else :
function ( ) #<-------- The action
if forceReturnToItem :
curBlock , curItem = forceReturnToItem
session . data . currentTrack ( ) . toItemInBlock ( curItem , curBlock ) #even works with appending positions (None)
if function == topLevelFunction :
callbacks . _updateChangedTracks ( ) #works with the current item @ cursor.
callbacks . _setCursor ( )
topLevelFunction = None
def insertItem ( item ) :
orderBeforeInsert = session . data . getBlockAndItemOrder ( )
moveFunction = _createLambdaMoveToForCurrentPosition ( )
if session . data . currentTrack ( ) . insert ( item ) :
def registeredUndoFunction ( ) :
moveFunction ( )
_changeBlockAndItemOrder ( orderBeforeInsert )
session . history . register ( registeredUndoFunction , descriptionString = f " Insert { item } " )
simpleCommand ( nothing , autoStepLeft = False ) #for callbacks
callbacks . _historyChanged ( )
def insertItemAtTickindex ( item , tickindex ) :
#session.data.currentTrack().goToTickindex
"""
Das ist eine conversion in der session . keine api befehle benutzen .
hier weiter machen . Der Cursor muss in Ruhe gelassen werden . Oder aber ich resette ohne callback ? das erscheint mir erstmal simpler .
Ich brauch aber eine Funktion goToTickindex im Track die Pausen erstellt wenn man da nicht hinkommt . Jetzt wünsch ich mir ich hätte die Noten nicht in ner liste sondern in einem dict mit tickindex . welp . . .
vielleicht brauch ich eine komplette parallelwelt . das midi modul sollte auf jeden fall nicht die keysig suchen müssen . kontext wird in der api hergestellt , nicht im midi
"""
def _createLambdaMoveToForCurrentPosition ( ) :
""" for undo only """
def moveTo ( trackIndex , blockIndex , localCursorIndexInBlock , pitchindex ) :
try :
session . data . goTo ( trackIndex , blockIndex , localCursorIndexInBlock )
session . data . cursor . pitchindex = pitchindex
except : #the worst that can happen is wrong movement.
pass
trackIndex , blockIndex , localCursorIndexInBlock = session . data . where ( )
moveFunction = lambda trIdx = trackIndex , blIdx = blockIndex , curIdx = localCursorIndexInBlock , pitchIdx = session . data . cursor . pitchindex : moveTo ( trIdx , blIdx , curIdx , pitchIdx )
return moveFunction
def _createLambdaRecreateSelection ( ) :
""" Not for undo.
Call it when you still have a selection , right after you do the
processing .
This is to keep a selection that changed the item positions but you want to support a new
call to the function immediately . For example the reorder notes by shuffling function .
The user will most likely hit that several times in a row to find something nice .
A valid selection implies that the cursor is on one end
of the selection . It doesn ' t matter which one but for the
sake of consistency and convenience we make sure that we end up with
the cursor on the bottomRight position .
It is entirely possible that the selection changed
the duration and the dimensions . The bottom right item might not
be on the same tickindex nor does it need to exist anymore .
Only the topLeft position is guaranteed to exist and have the same
tickindex . The item on that position however may not be there
anymore .
"""
def recreateSelection ( moveToFunction , topLeftCursor , bottomRightCursor ) :
session . data . cursorWhenSelectionStarted = None #take care of the current selection
moveToFunction ( )
session . data . goTo ( topLeftCursor [ " trackIndex " ] , topLeftCursor [ " blockindex " ] , topLeftCursor [ " localCursorIndex " ] )
session . data . setSelectionBeginning ( )
session . data . goTo ( bottomRightCursor [ " trackIndex " ] , bottomRightCursor [ " blockindex " ] , bottomRightCursor [ " localCursorIndex " ] )
assert session . data . cursorWhenSelectionStarted #only call this when you still have a selection
createSelectionFunction = lambda moveTo = _createLambdaMoveToForCurrentPosition ( ) , tL = session . data . cursor . exportObject ( session . data . currentTrack ( ) . state ) , bR = session . data . cursorWhenSelectionStarted , : recreateSelection ( moveTo , tL , bR )
return createSelectionFunction #When this function gets called we are back in the position that the newly generated data is selected, ready to to the complementary processing.
def _updateCallbackForListOfTrackIDs ( listOfChangedTrackIds ) :
for trackId in listOfChangedTrackIds :
callbacks . _updateTrack ( trackId )
def _updateCallbackAllTracks ( ) :
_updateCallbackForListOfTrackIDs ( session . data . listOfTrackIds ( ) )
updateCallbackAllTracks = _updateCallbackAllTracks
def _changeBlockAndItemOrder ( dictWithTrackIDsAndDataLists ) :
""" A helper function for deleteSelection, paste and other...
This makes it possible to undo / redo properly . It registers
itself with complementary data as undo / redo . """
orderBeforeInsert = session . data . getBlockAndItemOrder ( ) #save the current version as old version. as long as we keep this data around, e.g. in the undo stack, the items will not be truly deleted
moveFunction = _createLambdaMoveToForCurrentPosition ( ) #the goTo function for the cursor is not exactly in the right place. It may end up on the "other" side of a selection. Which is close enough.
def registeredUndoFunction ( ) :
moveFunction ( )
_changeBlockAndItemOrder ( orderBeforeInsert )
session . history . register ( registeredUndoFunction , descriptionString = " change order " )
#Replace old data with new parameter-data.
listOfTrackIDs = session . data . putBlockAndItemOrder ( dictWithTrackIDsAndDataLists )
#Make changes visible in the GUI
_updateCallbackForListOfTrackIDs ( listOfTrackIDs )
callbacks . _setCursor ( )
def cutObjects ( ) :
copyObjects ( )
_deleteSelection ( )
def _deleteSelection ( backspaceParamForCompatibilityIgnoreThis = None ) : #this is so special it gets its own command and is not part of the single delete() command.
orderBeforeDelete = session . data . getBlockAndItemOrder ( )
moveFunction = _createLambdaMoveToForCurrentPosition ( )
listOfChangedTrackIDs = session . data . deleteSelection ( )
if listOfChangedTrackIDs : #delete succeeded.
def registeredUndoFunction ( ) :
moveFunction ( )
_changeBlockAndItemOrder ( orderBeforeDelete )
session . history . register ( registeredUndoFunction , descriptionString = " delete selection " )
#Make changes visible in the GUI and midi
for trackId in listOfChangedTrackIDs :
callbacks . _updateTrack ( trackId )
callbacks . _setCursor ( )
def copyObjects ( ) : #ctrl+c
session . data . copyObjects ( )
#The score doesn't change at all. No callback.
#no undo.
def pasteObjects ( customBuffer = None , updateCursor = True , overwriteSelection = True ) : #ctrl+v
""" api.duplicate overrides default paste behaviour by providing its own copyBuffer
and not destroying the selection / keep the cursor at its origin position
"""
dataBefore = session . data . getBlockAndItemOrder ( )
moveFunction = _createLambdaMoveToForCurrentPosition ( )
listOfChangedTrackIDs = session . data . pasteObjects ( customBuffer , overwriteSelection )
if listOfChangedTrackIDs : #paste succeeded.
def registeredUndoFunction ( ) :
moveFunction ( )
_changeBlockAndItemOrder ( dataBefore )
session . history . register ( registeredUndoFunction , descriptionString = " paste " )
#Make changes visible in the GUI
for trackId in listOfChangedTrackIDs :
callbacks . _updateTrack ( trackId )
if updateCursor :
callbacks . _setCursor ( )
def pasteObjectsTransposedReal ( root : int = None , toPitch : int = None , adjustToKeySignature = False ) :
""" Uses the global/session clipboard buffer but pastes a transposed version, starting on the
pitch cursor position , that is adjusted to the current keysignature .
The notes are transformed into a custom buffer . We use standard paste for everything else .
If root or toPitch for the transposition interval are None they will derive their pitch from
the first pitch in the copy buffer and the cursor position , respectively .
"""
if not session . data . copyObjectsBuffer :
return #guard
copyBuffer = session . data . getIndenpendetCopyObjectsBuffer ( )
#Determine the first of the two pitches for our transposition interval
if root is None or not type ( root ) is int :
#First iteration only until the very first note, which we use to calculate the interval
for track in copyBuffer :
for item in track :
if type ( item ) is items . Chord :
root = item . notelist [ 0 ] . pitch #ordered by ascending pitch
break
else : #inner loop finished without break. No chord? in the first track.
logging . warning ( " Found copy buffer without note in the first track. This is worth an investigation. " )
continue #jump to the start and don't execute the outer loop break
break #outer loop. We only get here if the inner loop did break as well.
#Final Sanity Check. I don't think selections without chords are even allowed...
if root is None :
logging . error ( " pasteObjectsTransposedModal without notes in the copy buffer! (but not empty) " )
return
if toPitch is None :
toPitch = getCursorPitch ( )
keysig = session . data . currentTrack ( ) . state . keySignature ( )
#Edit the copy buffer in place. We don't modify the list, just the contents.
for track in copyBuffer :
for item in track :
if type ( item ) is items . Chord :
item . intervalAutomatic ( root , toPitch )
if adjustToKeySignature :
item . adjustToKeySignature ( [ keysig , ] ) #keysig must be in a list because it is a chord. If it is just len==1 the transpose function will deal with it correctly.
pasteObjects ( customBuffer = copyBuffer ) #normal paste except our special buffer
def pasteObjectsTransposedModal ( root : int = None , toPitch : int = None ) :
pasteObjectsTransposedReal ( root , toPitch , adjustToKeySignature = True )
def duplicate ( howOften : int ) : #ctrl+d
""" Duplicate a single object and put it right of the original. The cursor moves with it
to enable follow up insertion .
Duplicate the entire selection and put the copy right of the last selected note .
The cursor moves to selection start . deal with it .
Basically a special case of copy and paste that does not touch the clipboard . """
if session . data . cursorWhenSelectionStarted :
customBuffer = session . data . copyObjects ( writeInSessionBuffer = False )
if customBuffer : #success
session . data . goToSelectionStart ( )
pos = session . data . where ( ) #where even keeps the local position if a content linked block inserts items before our position
with session . history . sequence ( " duplicate selection " ) :
for i in range ( howOften ) :
pasteObjects ( customBuffer = customBuffer , updateCursor = False , overwriteSelection = False ) #handles undo
session . data . goTo ( * pos )
callbacks . _setCursor ( destroySelection = False )
else :
item = session . data . currentItem ( )
if item :
with session . history . sequence ( " duplicate item " ) :
for i in range ( howOften ) :
insertItem ( item . copy ( ) )
callbacks . _setCursor ( )
#Score
def transposeScore ( rootPitch , targetPitch ) :
""" Based on automatic transpose. The interval is caculated from two pitches.
There is also tranpose . But no transposeTrack , this is just select track and transpose . """
session . data . transposeScore ( rootPitch , targetPitch )
session . history . register ( lambda r = rootPitch , t = targetPitch : transposeScore ( t , r ) , descriptionString = " transpose score " )
callbacks . _historyChanged ( )
_updateCallbackAllTracks ( )
def useCurrentTrackAsMetronome ( ) :
""" This is called once after loading/creating a session in startEngine """
session . data . currentMetronomeTrack = session . data . currentTrack ( )
setMetronome ( session . data . currentMetronomeTrack . asMetronomeData , label = session . data . currentMetronomeTrack . name ) #template api. has callbacks
#Tracks
def insertTrack ( atIndex , trackObject ) :
moveFunction = _createLambdaMoveToForCurrentPosition ( )
newTrackId = id ( trackObject )
session . data . insertTrack ( atIndex , trackObject ) #side-effect: changes the active track to the new track which can be used in the next step:
def registeredUndoFunction ( ) :
moveFunction ( )
deleteTrack ( newTrackId )
session . history . register ( registeredUndoFunction , descriptionString = " insert track " )
session . data . calculateAudibleSoloForCbox ( )
callbacks . _tracksChanged ( )
callbacks . _updateTrack ( newTrackId )
callbacks . _setCursor ( )
#search tags: newTrack addTrack
def newEmptyTrack ( ) :
""" Append an empty track and switch to the new track """
newIndex = len ( session . data . tracks )
newTrack = Track ( session . data )
if newIndex > 0 :
newTrack . initialKeySignature = session . data . tracks [ 0 ] . initialKeySignature . copy ( )
newTrack . initialMetricalInstruction = session . data . tracks [ 0 ] . initialMetricalInstruction . copy ( )
newTrack . upbeatInTicks = session . data . tracks [ 0 ] . upbeatInTicks #just an int
insertTrack ( newIndex , newTrack ) #handles callbacks and undo
return ( id ( newTrack ) )
def deleteTrack ( trId ) :
""" Can not delete hidden tracks because these don ' t implement undo.
A hidden track is already considered " deleted " by the program .
Therefore you can only delete visible tracks . """
trackObject = session . data . trackById ( trId )
assert trackObject not in session . data . hiddenTracks #enforce docstring.
trackIndex = session . data . tracks . index ( trackObject )
didDelete = session . data . deleteTrack ( trackObject ) #may or may not change the trackindex. In any case, logically the cursor is now in a different track.
if didDelete : #score.deleteTrack does not delete the last remaining track
def registeredUndoFunction ( ) :
trackObject . sequencerInterface . recreateThroughUndo ( )
insertTrack ( trackIndex , trackObject )
session . history . register ( registeredUndoFunction , descriptionString = " delete track " )
callbacks . _tracksChanged ( )
callbacks . _setCursor ( )
if trackObject is session . data . currentMetronomeTrack :
useCurrentTrackAsMetronome ( ) #we already have a new current one
session . data . calculateAudibleSoloForCbox ( )
def deleteCurrentTrack ( ) :
deleteTrack ( id ( session . data . currentTrack ( ) ) )
def hideTrack ( trId ) :
""" For the callbacks this looks like a delete. But there is no undo.
The track still emits playback .
hide and unhide track register in the history . deleteItem depends on the item to be in a visible
track so it may possible to insert an item , hide the track and then undo which would try to
delete an item in a hidden track . Therefore hide and unhide register .
"""
trackObject = session . data . trackById ( trId )
result = session . data . hideTrack ( trackObject )
if result : #not the only track
session . history . register ( lambda trId = trId : unhideTrack ( trId ) , descriptionString = " hide track " )
callbacks . _tracksChanged ( ) #even if there is no change the GUI needs to be notified to redraw its checkboxes that may have been enabled GUI-side.
callbacks . _setCursor ( )
def unhideTrack ( trId ) :
trackObject = session . data . trackById ( trId )
session . data . unhideTrack ( trackObject ) #always succeeds, or throws error.
session . history . register ( lambda trId = trId : hideTrack ( trId ) , descriptionString = " unhide track " )
callbacks . _tracksChanged ( )
callbacks . _updateTrack ( trId )
#the cursor is uneffected
def trackAudible ( trId , state : bool ) :
"""
Aka . mute , but we don ' t call it like this because:
Send midi notes or not . CCs and instrument changes are unaffected .
Not managed by undo / redo .
Does not need updateTrack . There is no new midi data to generate . cbox handles mute on its own
Audible will shut off any output , no matter if solo or not .
Solo will determine which of the audible tracks are played .
Like in any DAW . Inverted Solo logic etc .
"""
session . data . trackById ( trId ) . audible = state
session . data . calculateAudibleSoloForCbox ( )
callbacks . _tracksChanged ( ) #even if there is no change the GUI needs to be notified to redraw its checkboxes that may have been enabled GUI-side.
def trackSolo ( trId , state : bool ) :
"""
Another layer like audible tracks .
This is the classic solo / mute duality .
Audible will shut off any output , no matter if solo or not .
Solo will determine which of the audible tracks are played .
Like in any DAW . Inverted Solo logic etc .
Not managed by undo / redo .
Does not need updateTrack . There is no new midi data to generate . cbox handles mute on its own
"""
session . data . trackById ( trId ) . solo = state
session . data . calculateAudibleSoloForCbox ( )
callbacks . _setCursor ( ) #the cursor includes solo export for the current track.
callbacks . _tracksChanged ( ) #even if there is no change the GUI needs to be notified to redraw its checkboxes that may have been enabled GUI-side.
def toggleCurrentTrackSolo ( ) :
""" trackSolo, but for the cursor. And toggle """
track = session . data . currentTrack ( )
trackSolo ( id ( track ) , not track . solo )
def resetAllSolo ( ) :
for track in session . data . tracks + list ( session . data . hiddenTracks . keys ( ) ) :
track . solo = False
session . data . calculateAudibleSoloForCbox ( )
callbacks . _setCursor ( ) #the cursor includes solo export for the current track.
callbacks . _tracksChanged ( ) #even if there is no change the GUI needs to be notified to redraw its checkboxes that may have been enabled GUI-side.
def listOfTrackIds ( ) :
return session . data . listOfTrackIds ( )
def listOfHiddenTrackIds ( ) :
return [ id ( track ) for track in session . data . hiddenTracks . keys ( ) ]
def rearrangeTracks ( listOfTrackIds ) :
if len ( session . data . tracks ) < = 1 :
return
session . history . register ( lambda l = session . data . asListOfTrackIds ( ) : rearrangeTracks ( l ) , descriptionString = " rearrange tracks " )
session . data . rearrangeTracks ( listOfTrackIds )
callbacks . _tracksChanged ( )
callbacks . _setCursor ( )
def setTrackName ( trId , nameString , initialInstrumentName , initialShortInstrumentName ) :
trackObject = session . data . trackById ( trId )
session . history . register ( lambda i = trId , n = trackObject . name , lyN = trackObject . initialInstrumentName , lySN = trackObject . initialShortInstrumentName : setTrackName ( i , n , lyN , lySN ) , descriptionString = " change track name " )
trackObject . name = nameString # this is a setter. It changes calfbox as well.
trackObject . initialInstrumentName = initialInstrumentName
trackObject . initialShortInstrumentName = initialShortInstrumentName
callbacks . _tracksChanged ( )
callbacks . _setCursor ( ) #cursor contains track name and thus needs updating
def setTrackUpbeat ( trId , upbeatInTicks ) :
trackObject = session . data . trackById ( trId )
session . history . register ( lambda i = trId , u = trackObject . upbeatInTicks : setTrackUpbeat ( i , u ) , descriptionString = " change track upbeat " )
trackObject . upbeatInTicks = upbeatInTicks
callbacks . _updateTrack ( trId )
def setDoubleTrack ( trId , statusBool ) :
""" It does not touch any important data because it is more or less
a savefile - persistent visual convenience feature .
So we don ' t need undo/redo " " "
trackObject = session . data . trackById ( trId )
trackObject . double = statusBool
callbacks . _tracksChanged ( )
callbacks . _updateTrack ( trId )
callbacks . _setCursor ( )
def setTrackSettings ( trId , dictionary ) :
""" We need to create a new playback of the track to update the midi data. """
trackObject = session . data . trackById ( trId )
previousSettings = trackObject . staticTrackRepresentation ( )
clean = True
for key , value in dictionary . items ( ) :
#this assumes keys are the same as track export. Will give a key error at least if not.
if not previousSettings [ key ] == value :
clean = False
break
if not clean :
trackObject . initialClefKeyword = dictionary [ " initialClefKeyword " ]
trackObject . initialMidiChannel = dictionary [ " initialMidiChannel " ]
trackObject . initialMidiBankMsb = dictionary [ " initialMidiBankMsb " ]
trackObject . initialMidiBankLsb = dictionary [ " initialMidiBankLsb " ]
trackObject . initialMidiProgram = dictionary [ " initialMidiProgram " ]
trackObject . ccChannels = dictionary [ " ccChannels " ]
trackObject . midiTranspose = dictionary [ " midiTranspose " ]
trackObject . durationSettingsSignature . defaultOn = dictionary [ " duration.defaultOn " ]
trackObject . durationSettingsSignature . defaultOff = dictionary [ " duration.defaultOff " ]
trackObject . durationSettingsSignature . staccatoOn = dictionary [ " duration.staccatoOn " ]
trackObject . durationSettingsSignature . staccatoOff = dictionary [ " duration.staccatoOff " ]
trackObject . durationSettingsSignature . tenutoOn = dictionary [ " duration.tenutoOn " ]
trackObject . durationSettingsSignature . tenutoOff = dictionary [ " duration.tenutoOff " ]
trackObject . durationSettingsSignature . legatoOn = dictionary [ " duration.legatoOn " ]
trackObject . durationSettingsSignature . legatoOff = dictionary [ " duration.legatoOff " ]
trackObject . dynamicSettingsSignature . dynamics [ " ppppp " ] = dictionary [ " dynamics.ppppp " ]
trackObject . dynamicSettingsSignature . dynamics [ " pppp " ] = dictionary [ " dynamics.pppp " ]
trackObject . dynamicSettingsSignature . dynamics [ " ppp " ] = dictionary [ " dynamics.ppp " ]
trackObject . dynamicSettingsSignature . dynamics [ " pp " ] = dictionary [ " dynamics.pp " ]
trackObject . dynamicSettingsSignature . dynamics [ " p " ] = dictionary [ " dynamics.p " ]
trackObject . dynamicSettingsSignature . dynamics [ " mp " ] = dictionary [ " dynamics.mp " ]
trackObject . dynamicSettingsSignature . dynamics [ " mf " ] = dictionary [ " dynamics.mf " ]
trackObject . dynamicSettingsSignature . dynamics [ " f " ] = dictionary [ " dynamics.f " ]
trackObject . dynamicSettingsSignature . dynamics [ " ff " ] = dictionary [ " dynamics.ff " ]
trackObject . dynamicSettingsSignature . dynamics [ " fff " ] = dictionary [ " dynamics.fff " ]
trackObject . dynamicSettingsSignature . dynamics [ " ffff " ] = dictionary [ " dynamics.ffff " ]
trackObject . dynamicSettingsSignature . dynamics [ " custom " ] = dictionary [ " dynamics.custom " ]
trackObject . dynamicSettingsSignature . dynamics [ " tacet " ] = dictionary [ " dynamics.tacet " ]
trackObject . dynamicSettingsSignature . dynamics [ " fp " ] = dictionary [ " dynamics.fp " ]
trackObject . dynamicSettingsSignature . dynamics [ " sp " ] = dictionary [ " dynamics.sp " ]
trackObject . dynamicSettingsSignature . dynamics [ " spp " ] = dictionary [ " dynamics.spp " ]
trackObject . dynamicSettingsSignature . dynamics [ " sfz " ] = dictionary [ " dynamics.sfz " ]
trackObject . dynamicSettingsSignature . dynamics [ " sf " ] = dictionary [ " dynamics.sf " ]
trackObject . dynamicSettingsSignature . dynamics [ " sff " ] = dictionary [ " dynamics.sff " ]
session . history . register ( lambda trId = trId , previousSettings = previousSettings : setTrackSettings ( trId , previousSettings ) , descriptionString = " change track settings " )
callbacks . _tracksChanged ( )
callbacks . _updateSingleTrackAllCC ( trId )
callbacks . _updateTrack ( trId )
callbacks . _setCursor ( ) #for midi channel RT thru
def resetDynamicSettingsSignature ( trId ) :
trackObject = session . data . trackById ( trId )
previousSettings = trackObject . staticTrackRepresentation ( )
trackObject . dynamicSettingsSignature . reset ( )
session . history . register ( lambda trId = trId , previousSettings = previousSettings : setTrackSettings ( trId , previousSettings ) , descriptionString = " reset track dynamic settings " )
callbacks . _tracksChanged ( )
#We only need this for midi changes. callbacks._updateSingleTrackAllCC(trId)
callbacks . _updateTrack ( trId )
def resetDuationSettingsSignature ( trId ) :
trackObject = session . data . trackById ( trId )
previousSettings = trackObject . staticTrackRepresentation ( )
trackObject . durationSettingsSignature . reset ( )
session . history . register ( lambda trId = trId , previousSettings = previousSettings : setTrackSettings ( trId , previousSettings ) , descriptionString = " reset track duration settings " )
callbacks . _tracksChanged ( )
#We only need this for midi changes. callbacks._updateSingleTrackAllCC(trId)
callbacks . _updateTrack ( trId )
#Blocks
def currentBlockExport ( ) :
""" Return the static export item of the current block.
Compatible with getDataAsDict and putDataFromDict
such as names and minimum tick duration """
return session . data . currentTrack ( ) . currentBlock ( ) . getDataAsDict ( )
def appendBlock ( trackid = None ) :
"""
Has dynamic behaviour :
If we are at the end of track appending a Block switches into
that new block .
On any other position it just appends a block and the cursor
stays where it is . This gives a better typing from left - to - right
experience . """
if trackid :
tr = session . data . trackById ( trackid )
else :
tr = session . data . currentTrack ( )
block = tr . appendBlock ( )
if tr . state . isAppending ( ) and len ( tr . blocks ) - 2 == tr . state . blockindex : #end of track?
moveFunction = _createLambdaMoveToForCurrentPosition ( )
def registeredUndoFunction ( ) :
moveFunction ( )
deleteBlock ( id ( block ) )
session . history . register ( registeredUndoFunction , descriptionString = " append block " )
session . data . currentTrack ( ) . right ( )
else :
session . history . register ( lambda blId = id ( block ) : deleteBlock ( blId ) , descriptionString = " append block " )
callbacks . _updateTrack ( id ( tr ) )
#callbacks._setCursor() #this will make the GUI jump around because it centers on the cursor
def splitBlock ( ) :
tr = session . data . currentTrack ( )
if tr . state . isAppending ( ) and len ( tr . blocks ) - 1 == tr . state . blockindex : #end of track? Yes len-1 here and len-2 in append() is correct. I don't know why :(
appendBlock ( )
else : #a real split
dictOfTrackIdsWithListOfBlockIds = session . data . splitCurrentBlockAndAllContentLinks ( ) #do the split. We get the current state as return value for undo
if dictOfTrackIdsWithListOfBlockIds :
rearrangeBlocksInMultipleTracks ( dictOfTrackIdsWithListOfBlockIds ) #handles undo and callbacks for redrawing
callbacks . _setCursor ( ) #cursor is still on the same item. But the item might be further to the right now when additional block boundaries have been introduced to the left through contentLinks
right ( ) # continue typing on the right side. This was added in 2020 to create a "split is an item" feeling
def joinBlockWithNext ( blockId ) :
""" written for the GUI which can join blocks with the mouse """
track , block = session . data . blockById ( blockId )
session . data . goTo ( session . data . tracks . index ( track ) , track . blocks . index ( block ) , 0 )
joinBlock ( )
def joinBlock ( ) :
""" Opposite of splitBlock (but not undo)
Take the current block and join it with the one after it .
Does the same automatically for all content linked blocks .
If there is no next block this will do nothing .
It only works on content linked blocks if ALL content linked blocks have a different
content linked block after them . So a block A needs always to be