You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2524 lines
111 KiB
2524 lines
111 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2020, 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
|
|
|
|
#Third Party Modules
|
|
|
|
#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 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):
|
|
_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()
|
|
|
|
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():
|
|
return session.data.metaData
|
|
|
|
def setMetadata(data):
|
|
session.data.metaData = data
|
|
|
|
def playFromCursor():
|
|
playFrom(ticks=session.data.cursorExport()["tickindex"])
|
|
|
|
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 duplicate(): #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
|
|
pasteObjects(customBuffer = customBuffer, updateCursor = False, overwriteSelection = False) #handles undo
|
|
session.data.goTo(*pos)
|
|
callbacks._setCursor(destroySelection=False)
|
|
else:
|
|
item = session.data.currentItem()
|
|
if item:
|
|
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")
|
|
callbacks._tracksChanged()
|
|
callbacks._updateTrack(newTrackId)
|
|
callbacks._setCursor()
|
|
|
|
def newEmptyTrack():
|
|
"""Append an empty track and switch to the new track"""
|
|
newIndex = len(session.data.tracks)
|
|
newTrack = Track(session.data)
|
|
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
|
|
|
|
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):
|
|
"""
|
|
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"""
|
|
trackObject = session.data.trackById(trId)
|
|
trackObject.sequencerInterface.enable(state)
|
|
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()
|
|
|
|
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.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 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()
|
|
|
|
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
|
|
|
|
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 followed by block B.
|
|
Joining those will result in one big new content linked block in the whole track.
|
|
It is exactly the same as splitting a content linked block which will result in two new
|
|
content links."""
|
|
where = session.data.where() #just for the user experience.
|
|
|
|
dictOfTrackIdsWithListOfBlockIds = session.data.joinCurrentBlockAndAllContentLinks()
|
|
if dictOfTrackIdsWithListOfBlockIds:
|
|
rearrangeBlocksInMultipleTracks(dictOfTrackIdsWithListOfBlockIds) #handles undo and callbacks for redrawing
|
|
|
|
session.data.goTo(*where) #just for the user experience.
|
|
callbacks._setCursor()
|
|
|
|
def deleteBlock(blockId):
|
|
track, block = session.data.blockById(blockId)
|
|
oldBlockArrangement = track.asListOfBlockIds()
|
|
parentTrack, deletedBlock = track.deleteBlock(block)
|
|
if (parentTrack and deletedBlock): #not the last block
|
|
#Blocks are never truly deleted but a stored in the Block.allBlocks dict. This keeps the reference to this deleted block alive and it can be added through rearrange, which gets its blocks from this dict.
|
|
session.history.register(lambda i=id(track), l=oldBlockArrangement: rearrangeBlocks(i, l), descriptionString = "delete block")
|
|
callbacks._updateTrack(id(track))
|
|
callbacks._setCursor()
|
|
|
|
def deleteCurrentBlock():
|
|
currentBlockId = id(session.data.currentTrack().currentBlock())
|
|
deleteBlock(currentBlockId) #handles callbacks and undo
|
|
|
|
def deleteEmptyBlocks():
|
|
"""whole score"""
|
|
dictOfTrackIdsWithListOfBlockIds = session.data.removeEmptyBlocks()
|
|
#We keep everything in dictOfTrackIdsWithListOfBlockIds. Empty blocks have been removed from this data.
|
|
if dictOfTrackIdsWithListOfBlockIds and all(l for l in dictOfTrackIdsWithListOfBlockIds.values()):
|
|
rearrangeBlocksInMultipleTracks(dictOfTrackIdsWithListOfBlockIds) #handles undo and callbacks for redrawing
|
|
callbacks._setCursor()
|
|
|
|
def duplicateCurrentBlock():
|
|
currentBlockId = id(session.data.currentTrack().currentBlock())
|
|
duplicateBlock(currentBlockId, 1) #handles callbacks and undo
|
|
|
|
def duplicateContentLinkCurrentBlock():
|
|
currentBlockId = id(session.data.currentTrack().currentBlock())
|
|
duplicateContentLinkBlock(currentBlockId, 1) #handles callbacks and undo
|
|
|
|
def duplicateBlock(blockId, times = 1):
|
|
track, block = session.data.blockById(blockId)
|
|
session.history.register(lambda i=id(track), l=track.asListOfBlockIds(): rearrangeBlocks(i, l), descriptionString = "duplicate block")
|
|
for i in range(times):
|
|
track.duplicateBlock(block)
|
|
callbacks._updateTrack(id(track))
|
|
callbacks._setCursor()
|
|
|
|
def duplicateContentLinkBlock(blockId, times = 1):
|
|
track, block = session.data.blockById(blockId)
|
|
session.history.register(lambda i=id(track), l=track.asListOfBlockIds(): rearrangeBlocks(i, l), descriptionString = "content link block")
|
|
for i in range(times):
|
|
track.duplicateContentLinkBlock(block)
|
|
callbacks._updateTrack(id(track))
|
|
callbacks._setCursor()
|
|
|
|
def moveBlockToOtherTrack(blockId, newTrackId, listOfBlockIdsForNewTrack):
|
|
"""First move the block to the new track and then
|
|
rearrange both tracks
|
|
|
|
It is by design only possible that a block will be copied/linked
|
|
in the same track, next to the original block.
|
|
If you want the dual action of "copy this block to a new track" you need to do it in two steps.
|
|
First copy. It can be moved later by this api function."""
|
|
oldTrack, block = session.data.blockById(blockId)
|
|
assert oldTrack
|
|
assert block
|
|
|
|
if len(oldTrack.blocks) == 1:
|
|
return False #it is not possible to move the last block.
|
|
|
|
session.history.register(lambda blId=blockId, trId=id(oldTrack), l=oldTrack.asListOfBlockIds(): moveBlockToOtherTrack(blId, trId, l), descriptionString = "move block to other track")
|
|
newTrack = session.data.trackById(newTrackId)
|
|
newTrack.appendExistingBlock(block)
|
|
newTrack.rearrangeBlocks(listOfBlockIdsForNewTrack)
|
|
#We don't need to check if deleting succeeded because we already checked if there are more than 1 blocks in the track above.
|
|
oldTrack.deleteBlock(block) #It is important that we delete the block at exactly this point in time, not ealier. Otherwise the reference for undo will go away.
|
|
block.parentTrack = newTrack
|
|
|
|
callbacks._updateTrack(id(oldTrack))
|
|
callbacks._updateTrack(newTrackId)
|
|
callbacks._setCursor()
|
|
|
|
def rearrangeBlocks(trackid, listOfBlockIds):
|
|
track = session.data.trackById(trackid)
|
|
oldBlockOrder = track.asListOfBlockIds()
|
|
track.rearrangeBlocks(listOfBlockIds)
|
|
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
rearrangeBlocks(trackid, oldBlockOrder)
|
|
session.history.register(registeredUndoFunction, descriptionString = "rearrange blocks")
|
|
callbacks._updateTrack(trackid)
|
|
|
|
def rearrangeBlocksInMultipleTracks(dictOfTrackIdsWithListOfBlockIds):
|
|
"""dictOfTrackIdsWithListOfBlockIds is [trackId] = [listOfBlockIds]"""
|
|
forUndo = {}
|
|
for trackId, listOfBlockIds in dictOfTrackIdsWithListOfBlockIds.items():
|
|
track = session.data.trackById(trackId)
|
|
oldBlockOrder = track.asListOfBlockIds()
|
|
track.rearrangeBlocks(listOfBlockIds)
|
|
forUndo[trackId] = oldBlockOrder
|
|
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
rearrangeBlocksInMultipleTracks(forUndo)
|
|
session.history.register(registeredUndoFunction, descriptionString = "multi-track rearrange block")
|
|
|
|
for trackId in dictOfTrackIdsWithListOfBlockIds.keys():
|
|
callbacks._updateTrack(trackId)
|
|
callbacks._setCursor()
|
|
|
|
def changeBlock(blockId, newParametersDict):
|
|
"""for example "name" or "minimumInTicks" """
|
|
track, block = session.data.blockById(blockId)
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
changeBlock(blockId, block.getDataAsDict())
|
|
session.history.register(registeredUndoFunction, descriptionString = "change block")
|
|
block.putDataFromDict(newParametersDict)
|
|
callbacks._updateTrack(id(track))
|
|
callbacks._setCursor()
|
|
|
|
def unlinkCurrentBlock():
|
|
currentBlockId = id(session.data.currentTrack().currentBlock())
|
|
unlinkBlock(currentBlockId)
|
|
|
|
def unlinkBlock(blockId):
|
|
track, block = session.data.blockById(blockId)
|
|
newData = block.getUnlinkedData()
|
|
assert newData
|
|
_setBlockData(block, newData) #handles undo and callbacks
|
|
|
|
def _setBlockData(block, newData):
|
|
session.history.register(lambda bl=block, old=block.data: _setBlockData(bl, old), descriptionString = "set block data")
|
|
block.data = newData
|
|
#no callbacks needed.
|
|
|
|
|
|
#Cursor
|
|
def left():
|
|
"""move the currently active tracks cursor one position to the left.
|
|
Can be directly used by a user interface"""
|
|
session.data.currentTrack().left()
|
|
callbacks._setCursor()
|
|
|
|
def right():
|
|
"""move the currently active tracks cursor one position to the right.
|
|
Can be directly used by a user interface"""
|
|
session.data.currentTrack().right()
|
|
callbacks._setCursor()
|
|
|
|
def selectLeft():
|
|
session.data.setSelectionBeginning() #or not, if there is already one.
|
|
session.data.currentTrack().left()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectRight():
|
|
session.data.setSelectionBeginning()
|
|
session.data.currentTrack().right()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def measureLeft():
|
|
"""Goes a few steps left when not in metrical context"""
|
|
session.data.currentTrack().measureLeft()
|
|
callbacks._setCursor()
|
|
|
|
def measureRight():
|
|
"""Goes a few right left when not in metrical context"""
|
|
session.data.currentTrack().measureRight()
|
|
callbacks._setCursor()
|
|
|
|
def selectMeasureLeft():
|
|
session.data.setSelectionBeginning()
|
|
session.data.currentTrack().measureLeft()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectMeasureRight():
|
|
session.data.setSelectionBeginning()
|
|
session.data.currentTrack().measureRight()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def measureStart():
|
|
session.data.currentTrack().measureStart()
|
|
callbacks._setCursor(destroySelection = True)
|
|
|
|
def selectMeasureStart():
|
|
session.data.setSelectionBeginning()
|
|
session.data.currentTrack().measureStart()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
|
|
def blockLeft():
|
|
if session.data.currentTrack().currentBlock().localCursorIndex == 0:
|
|
session.data.currentTrack().left()
|
|
session.data.currentTrack().startOfBlock()
|
|
session.data.currentTrack().left()
|
|
callbacks._setCursor()
|
|
|
|
def blockRight():
|
|
if session.data.currentTrack().currentBlock().isAppending():
|
|
session.data.currentTrack().right()
|
|
session.data.currentTrack().endOfBlock()
|
|
session.data.currentTrack().right()
|
|
callbacks._setCursor()
|
|
|
|
def selectBlockLeft():
|
|
if session.data.currentTrack().currentBlock().localCursorIndex == 0:
|
|
session.data.currentTrack().left()
|
|
session.data.setSelectionBeginning()
|
|
session.data.currentTrack().startOfBlock()
|
|
session.data.currentTrack().left()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectBlockRight():
|
|
if session.data.currentTrack().currentBlock().isAppending():
|
|
session.data.currentTrack().right()
|
|
session.data.setSelectionBeginning()
|
|
session.data.currentTrack().endOfBlock()
|
|
session.data.currentTrack().right()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def head():
|
|
session.data.currentTrack().head()
|
|
callbacks._setCursor()
|
|
|
|
def tail():
|
|
session.data.currentTrack().tail()
|
|
callbacks._setCursor()
|
|
|
|
def selectHead():
|
|
session.data.setSelectionBeginning()
|
|
session.data.currentTrack().head()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectTail():
|
|
session.data.setSelectionBeginning()
|
|
session.data.currentTrack().tail()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def trackUp():
|
|
session.data.trackUp()
|
|
callbacks._setCursor()
|
|
|
|
def trackDown():
|
|
session.data.trackDown()
|
|
callbacks._setCursor()
|
|
|
|
def trackFirst():
|
|
session.data.trackFirst()
|
|
callbacks._setCursor()
|
|
|
|
def trackLast():
|
|
session.data.trackLast()
|
|
callbacks._setCursor()
|
|
|
|
def selectTrackUp():
|
|
session.data.setSelectionBeginning()
|
|
session.data.trackUp()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectTrackDown():
|
|
session.data.setSelectionBeginning()
|
|
session.data.trackDown()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectTrackFirst():
|
|
session.data.setSelectionBeginning()
|
|
session.data.trackFirst()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectTrackLast():
|
|
session.data.setSelectionBeginning()
|
|
session.data.trackLast()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectAllTracks():
|
|
shortestTrack = sorted(session.data.tracks, key = lambda track: track.duration())[0]
|
|
trackIndex = session.data.tracks.index(shortestTrack)
|
|
session.data.goTo(trackIndex, 0, 0) #the position in the track doesn't matter. we just want into the track.
|
|
tail()
|
|
tickindexShortestTrack = session.data.currentTrack().state.tickindex
|
|
#assert session.data.currentTrack().state.tickindex == tickindexShortestTrack == shortestTrack.duration(), (session.data.currentTrack().state.tickindex, tickindexShortestTrack, shortestTrack.duration())
|
|
session.data.trackFirst()
|
|
session.data.currentTrack().head()
|
|
#Create Selection
|
|
session.data.setSelectionBeginning()
|
|
session.data.trackLast()
|
|
session.data.currentTrack().goToTickindex(tickindexShortestTrack)
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def selectTrack():
|
|
head()
|
|
selectTail()
|
|
|
|
def selectMeasureColumn():
|
|
measureStart()
|
|
trackFirst()
|
|
measureStart()
|
|
selectMeasureRight()
|
|
selectTrackLast()
|
|
|
|
def toTickindex(trackid, tickindex, destroySelection = True):
|
|
"""Was implemented for mouse clicking in a GUI Score"""
|
|
if tickindex < 0:
|
|
tickindex = 0
|
|
session.data
|
|
trackObject = session.data.trackById(trackid)
|
|
session.data.trackIndex = session.data.tracks.index(trackObject)
|
|
trackObject.goToTickindex(tickindex)
|
|
callbacks._setCursor(destroySelection)
|
|
|
|
def selectToTickindex(trackid, tickindex):
|
|
session.data.setSelectionBeginning()
|
|
toTickindex(trackid, tickindex, destroySelection = False)
|
|
|
|
def up():
|
|
session.data.cursor.up()
|
|
callbacks._setCursor(destroySelection = False)
|
|
#this does not create a selection if there was none.
|
|
#You need to call setSelectionBeginning() to start a selection.
|
|
#However, this does modify an existing selection since the cursor value changes
|
|
|
|
def down():
|
|
session.data.cursor.down()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def upOctave():
|
|
session.data.cursor.upOctave()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def downOctave():
|
|
session.data.cursor.downOctave()
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def _delete(backspace = False):
|
|
def undoDelete(deletedItemCopy):
|
|
"""We need to go back one position after insert"""
|
|
insertItem(deletedItemCopy)
|
|
left() #sets the cursor
|
|
|
|
if backspace: #this is not in backspace() itself because it disturbs _deleteSelection() when we go left before deleting.
|
|
session.data.currentTrack().left() #does not trigger a callback.
|
|
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
deletedItemCopy = session.data.currentTrack().delete() #this is obviously _not_ track-delete but item delete
|
|
if deletedItemCopy: #and not None / appending position
|
|
if backspace:
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
insertItem(deletedItemCopy)
|
|
session.history.register(registeredUndoFunction, descriptionString = "delete item")
|
|
else:
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
undoDelete(deletedItemCopy)
|
|
session.history.register(registeredUndoFunction, descriptionString = "delete item")
|
|
callbacks._updateChangedTracks()
|
|
callbacks._setCursor()
|
|
|
|
def backspace():
|
|
"""Callback and Undo are done via delete"""
|
|
delete(backspaceForSingleItemDelete = True)
|
|
|
|
def delete(backspaceForSingleItemDelete = None):
|
|
"""Choose wether to delete a single item or a selection.
|
|
Delete selections is tricky thats why this is done not
|
|
in the usual apply-function-to-selection way"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_deleteSelection()
|
|
else:
|
|
_delete(backspaceForSingleItemDelete)
|
|
|
|
#Chose prevailing durations
|
|
|
|
def prevailingLonga():
|
|
pass #TODO
|
|
def prevailingBrevis():
|
|
pass #TODO
|
|
def prevailing1():
|
|
session.data.cursor.prevailingBaseDuration = D1
|
|
callbacks._prevailingBaseDurationChanged(D1)
|
|
def prevailing2():
|
|
session.data.cursor.prevailingBaseDuration = D2
|
|
callbacks._prevailingBaseDurationChanged(D2)
|
|
def prevailing4():
|
|
session.data.cursor.prevailingBaseDuration = D4
|
|
callbacks._prevailingBaseDurationChanged(D4)
|
|
def prevailing8():
|
|
session.data.cursor.prevailingBaseDuration = D8
|
|
callbacks._prevailingBaseDurationChanged(D8)
|
|
def prevailing16():
|
|
session.data.cursor.prevailingBaseDuration = D16
|
|
callbacks._prevailingBaseDurationChanged(D16)
|
|
def prevailing32():
|
|
session.data.cursor.prevailingBaseDuration = D32
|
|
callbacks._prevailingBaseDurationChanged(D32)
|
|
|
|
|
|
##Chords and duration modification
|
|
|
|
def insertChord(baseDuration, pitchToInsert):
|
|
duration = items.Duration(baseDuration)
|
|
#duration.dots = session.data.cursor.getPrevailingDot()
|
|
duration.dots = 0
|
|
|
|
#audio Feedback as callback? #TODO
|
|
chord = items.Chord(duration, pitchToInsert)
|
|
insertItem(chord)
|
|
|
|
def insertCursorChord(baseDuration):
|
|
"""Insert a new chord with one note to the track.
|
|
The intial note gets its pitch from the cursor position, to scale"""
|
|
keysig = session.data.currentTrack().state.keySignature()
|
|
pitchToInsert = pitchmath.toScale(session.data.cursor.pitchindex, keysig)
|
|
insertChord(baseDuration, pitchToInsert)
|
|
|
|
def addNoteToChord(pitchToInsert):
|
|
"""Utilized by midi-in"""
|
|
#TODO: in the past this was possible with prevailing duration as well. But it got too complex with undo. Just pitch for now.
|
|
_applyToItem("addNote", parameters = [pitchToInsert])
|
|
|
|
def addCursorNoteToChord():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
return False
|
|
else:
|
|
keysig = session.data.currentTrack().state.keySignature()
|
|
pitchToInsert = pitchmath.toScale(session.data.cursor.pitchindex, keysig)
|
|
_applyToItem("addNote", parameters = [pitchToInsert])
|
|
|
|
def deleteCursorNoteFromChord():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
return False
|
|
else:
|
|
_applyToItem("removeNoteNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
|
|
def insertBrevis():
|
|
pass #TODO
|
|
def insertLonga():
|
|
pass #TODO
|
|
def insert1():
|
|
insertCursorChord(D1)
|
|
def insert2():
|
|
insertCursorChord(D2)
|
|
def insert4():
|
|
insertCursorChord(D4)
|
|
def insert8():
|
|
insertCursorChord(D8)
|
|
def insert16():
|
|
insertCursorChord(D16)
|
|
def insert32():
|
|
insertCursorChord(D32)
|
|
def insert64():
|
|
insertCursorChord(D64)
|
|
def insert128():
|
|
insertCursorChord(D128)
|
|
|
|
#and so on
|
|
|
|
functionType = type(lambda: print()) #not a built-in function or method, just <class "function">
|
|
def _applyToItem(functionAsString, parameters = []):
|
|
"""
|
|
see history.py / undo docstring
|
|
|
|
This is the entry point. Further undo and redo are done by
|
|
_applyToItemCircularUndoRedo()
|
|
|
|
Internal functions that do something but do not return an undoFunction
|
|
are considered broken. Not returning an undo function is assumed
|
|
as "nothing happened, nothing changed".
|
|
|
|
"""
|
|
#TODO: If getattr with string turns out to be a performance problem we must change that to a lambda function as only parameter and the api function has to get the item itself. Then we can use _applyToItemCircularUndoRedo directly (and rename it)
|
|
def do():
|
|
i = session.data.currentItem()
|
|
if not i: return None #Appending Position or otherwise no item.
|
|
undoFunction = getattr(i, functionAsString)(*parameters) #Get the real function, execute it and take the return as undo function. Each function returns an undo function (if it supports undo. see below)
|
|
if undoFunction: #the command was supported by the item. e.g. rests don't support shiftUp.
|
|
assert type(undoFunction) is functionType
|
|
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
_applyToItemCircularUndoRedo(undoFunction)
|
|
|
|
session.history.register(registeredUndoFunction, descriptionString = functionAsString)
|
|
simpleCommand(do)
|
|
|
|
|
|
def _applyToItemCircularUndoRedo(function):
|
|
"""called by _applyToItem and by itself.
|
|
Function must be a command that can precisely undo itself:
|
|
see history / undo docstring
|
|
|
|
Toggles (e.g. bools) are not suitable undo functions.
|
|
For example beamGroups. They are just True and False, so at first glance you could think that
|
|
"do" is toggle so undo is toggle as well. But "do" can also be "removeBeams" in a selection.
|
|
A precise function is a function that stores or restores the exact old state as lambda parameter
|
|
or similar.
|
|
"""
|
|
def do():
|
|
undoFunction = function()
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
_applyToItemCircularUndoRedo(undoFunction)
|
|
session.history.register(registeredUndoFunction, descriptionString = function.__name__ )
|
|
simpleCommand(do)
|
|
|
|
|
|
def _applyToSelection(itemFunctionAsString, parameterListForEachSelectedItem = []):
|
|
"""
|
|
Apply a function to a selection and undo.
|
|
The function must not rely on track.state, especially not on
|
|
the current keysignature etc.
|
|
|
|
Even if the functions add or remove the content (like api.split does) it will work.
|
|
|
|
There is an optional parameter which is a list of parameter-iterables.
|
|
The top level list must have exactly the len() as items in selection.
|
|
|
|
It was written for undo because you know the number of items
|
|
exactly here. This function itself creates this list for undo/redo.
|
|
|
|
Format of parameterListForEachSelectedItem. In this example we already know
|
|
that we apply to a selection with 3 items.:
|
|
|
|
Our function expects two parameters, float and bool.
|
|
parameterSet_One = [parameterValue=3.1415, parameterVerbose=True]
|
|
parameterSet_Two = [parameterValue=42.42, parameterVerbose=False]
|
|
parameterSet_Three = [parameterValue=100.001, parameterVerbose=False]
|
|
parameterListForEachSelectedItem = [parameterSet_One, parameterSet_Two, parameterSet_Three]
|
|
_applyToSelection("doFunction", parameterListForEachSelectedItem)
|
|
|
|
You also can use a len of one for the parameterListForEachSelectedItem. It will be
|
|
applied to all items in the selection.
|
|
onlyParameterSet = [parameterValue=42.0, parameterVerbose=True]
|
|
parameterListForAllSelectedItem = [onlyParameterSet]
|
|
_applyToSelection("doFunction", parameterListForAllSelectedItem)
|
|
|
|
|
|
Or, a typical case, just one parameter, the same, for each item in
|
|
the selection. The parameter itself can be a list itself as well.
|
|
For example a duration.tuplets, which is list of tuples(!)
|
|
theParameter = [[(2,3)]] #This is a tuplet with only one fraction, triplet. It is for a notelist with only one note.
|
|
#theParameter = [ [(2,3), (4,5)], [(4,5), (1,2), (3,4)] ] #notelist of 2 with a double-nested tuplet for the first and a triple nested tuplet for the second note.
|
|
onlyParameterSet = [theParameter]
|
|
parameterListForAllSelectedItem = [onlyParameterSet]
|
|
_applyToSelection("setTuplet", parameterListForAllSelectedItem)
|
|
|
|
Now look how that would look written out, especially since there is only one parameter.
|
|
It is crazy, but neccessary.
|
|
_applyToSelection("setTuplet", [[[[(2,3)]]]])
|
|
"""
|
|
|
|
def _replaceMagicStringParameters(listOfParameters, cachedTrackState):
|
|
"""
|
|
Replaces the magic strings in our parameterlist
|
|
with the actual values saved for this moment by
|
|
score.listOfSelectedItems()
|
|
|
|
listOfParameters is a list with parameters for the
|
|
called function. This function is sometimes another list.
|
|
For example a parameter for each note in a chord.
|
|
"""
|
|
result = []
|
|
for param in listOfParameters:
|
|
if type(param) is list:
|
|
result.append(_replaceMagicStringParameters(param, cachedTrackState))
|
|
else:
|
|
if param in cachedTrackState:
|
|
result.append(cachedTrackState[param])
|
|
else:
|
|
result.append(param)
|
|
return result
|
|
|
|
withMagicString = any(type(elem) is str for elem in flatList(parameterListForEachSelectedItem))
|
|
|
|
validSelection, topLeftCursor, bottomRightCursor, listOfChangedTrackIds, *selectedTracksAndItems = session.data.listOfSelectedItems(removeContentLinkedData = True)
|
|
if validSelection:
|
|
if len(parameterListForEachSelectedItem) == 1: #only one value. Use it for all function calls. This is expected most of the time when we get a value from the user directly. Like "make all notes quintuplets!".
|
|
parameterGenerator = EndlessGenerator(parameterListForEachSelectedItem[0])
|
|
else:
|
|
parameterGenerator = (iterable for iterable in parameterListForEachSelectedItem) #we make a generator because that works across tracks in the nested for loop. zip() does not.
|
|
|
|
cursorWasAtSelectionStart = session.data.goToSelectionEnd() #Moves the cursor to the bottom right end of the selection. This alone will make the selection correct at the end of processing if the duration was changed. But it is still not enough for functions that change the number of items in the selection, e.g. split
|
|
#We may be in a different track now.
|
|
trackIndex, blockIndex, localCursorIndexInBlock = session.data.where() #localCursorIndexInBlock is unreliable...
|
|
itemRightOfTheSelection = session.data.currentTrack().currentItem() #so we replace it with an item. Since the selection ends left of the current item (current one not included) it cannot be affected by the process below. it is safe to remember as marker.
|
|
|
|
undoFunctions = []
|
|
for track in selectedTracksAndItems:
|
|
for item, cachedTrackState in track: #cachedTrackState is a selected set of cached track.state values, specifically designed to assist apply to selection
|
|
listOfParameters = next(parameterGenerator) if parameterListForEachSelectedItem else ()
|
|
assert listOfParameters.__iter__ #will not throw an assert but an AttributeError. But who cares.
|
|
if withMagicString:
|
|
listOfParameters = _replaceMagicStringParameters(listOfParameters, cachedTrackState)
|
|
undoFunction = getattr(item, itemFunctionAsString)(*listOfParameters) #ACTION!
|
|
|
|
if undoFunction: #the command was supported by the item. e.g. rests don't support shiftUp.
|
|
assert type(undoFunction) is functionType
|
|
undoFunctions.append(undoFunction)
|
|
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
_applyToSelectionCircularRedoUndo(undoFunctions, listOfChangedTrackIds)
|
|
session.history.register(registeredUndoFunction, descriptionString = "selection: {}".format(itemFunctionAsString))
|
|
|
|
#Go to the end of the selection + all new items and durations.
|
|
session.data.goToItemInCurrentBlock(itemRightOfTheSelection)
|
|
_updateCallbackForListOfTrackIDs(listOfChangedTrackIds)
|
|
|
|
#The selection now has the right boundaries. Now we return the cursor where it was for the user.
|
|
if cursorWasAtSelectionStart: #we were at the selection start before processing. returning...
|
|
session.data.goToSelectionStart() #keeps the selection as it is, just switches the cursor position from one end of the S. to the other.
|
|
|
|
callbacks._setCursor(destroySelection = False)
|
|
|
|
def _applyToSelectionCircularRedoUndo(functions, listOfChangedTrackIds):
|
|
"""
|
|
Functions are a list of functions which don't need any parameters.
|
|
They are defined as lambdas with preset parameters.
|
|
|
|
We don't need a selection for actually making applyToSelection undo.
|
|
All affected tracks, items, methods and values are already known.
|
|
|
|
Functions must not be be apply-to-selection functions. To avoid
|
|
callback confusion best practice is to use no api function at all
|
|
in the functions list.
|
|
|
|
When this was written there were only score/track/item functions
|
|
possible at all. Keep it that way.
|
|
"""
|
|
session.data.cursorWhenSelectionStarted = None #better safe than sorry. It will be destroyed by setCursor below anyway.
|
|
undoFunctions = [f() for f in functions] #side effect: make undo happen.
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
_applyToSelectionCircularRedoUndo(undoFunctions, listOfChangedTrackIds)
|
|
session.history.register(registeredUndoFunction, descriptionString = "selection stuff")
|
|
_updateCallbackForListOfTrackIDs(listOfChangedTrackIds)
|
|
callbacks._setCursor()
|
|
|
|
def augment():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("augment")
|
|
else:
|
|
_applyToItem("augmentNoteNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def diminish():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("diminish")
|
|
else:
|
|
_applyToItem("diminishNoteNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def dot():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("toggleDot")
|
|
else:
|
|
_applyToItem("toggleDotNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def triplet():
|
|
"""
|
|
From anything (empty, other tuplet) to triplet
|
|
and back to empty"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("toggleTriplet")
|
|
else:
|
|
_applyToItem("toggleTripletNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def quintuplet():
|
|
"""
|
|
Make it a quintuplet, no toggle
|
|
!setTuplet has a different parameter format than setTupletNearPitch!
|
|
"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
tupletListForDuration = [[[[(4,5)]]]] #see docstring for _applyToSelection
|
|
_applyToSelection("setTuplet", parameterListForEachSelectedItem = tupletListForDuration)
|
|
else:
|
|
tupletListForSingleNoteDuration = [(4,5)]
|
|
_applyToItem("setTupletNearPitch", parameters = [session.data.cursor.pitchindex, tupletListForSingleNoteDuration])
|
|
|
|
def tie():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("toggleDurationKeyword", parameterListForEachSelectedItem = [[[D_TIE]]])
|
|
else:
|
|
_applyToItem("toggleDurationKeywordNearPitch", parameters = [session.data.cursor.pitchindex, D_TIE])
|
|
|
|
def tenuto():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("toggleDurationKeyword", parameterListForEachSelectedItem = [[[D_TENUTO]]])
|
|
else:
|
|
_applyToItem("toggleDurationKeywordNearPitch", parameters = [session.data.cursor.pitchindex, D_TENUTO])
|
|
|
|
def staccato():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("toggleDurationKeyword", parameterListForEachSelectedItem = [[[D_STACCATO]]])
|
|
else:
|
|
_applyToItem("toggleDurationKeywordNearPitch", parameters = [session.data.cursor.pitchindex, D_STACCATO])
|
|
|
|
def split(newparts):
|
|
"""
|
|
Split a chord into two or more.
|
|
This is only for chords.
|
|
Technically this works with rests as well.
|
|
However, there is little musical meaning in splitted rests.
|
|
And it does not make sense for further editing.
|
|
"""
|
|
|
|
#TODO: Test this for the whole selection before doing something. And as always: test first, not during performing the changes.
|
|
#if currentItem.durationGroup.hasTuplet() and not newparts in (2,4,8):
|
|
#return False #no further splitting
|
|
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("split", parameterListForEachSelectedItem = [[newparts]],) #chord.split does not expect a list as parameter. just one parameter.
|
|
#Split introduces more items. The selection needs extension.
|
|
|
|
else:
|
|
_applyToItem("split", parameters = [newparts])
|
|
for i in range(newparts):
|
|
right()
|
|
|
|
#Pitch modifications
|
|
def sharpenNote():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("sharpen")
|
|
else:
|
|
_applyToItem("sharpenNoteNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def flattenNote():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("flatten")
|
|
else:
|
|
_applyToItem("flattenNoteNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
|
|
def stepUp():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("stepUp", parameterListForEachSelectedItem = [[["keySignature"]]])
|
|
else:
|
|
_applyToItem("stepUpNoteNearPitch", parameters = [session.data.cursor.pitchindex, session.data.currentTrack().state.keySignature()])
|
|
up()
|
|
|
|
|
|
def stepDown():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("stepDown", parameterListForEachSelectedItem = [[["keySignature"]]])
|
|
else:
|
|
_applyToItem("stepDownNoteNearPitch", parameters = [session.data.cursor.pitchindex, session.data.currentTrack().state.keySignature()])
|
|
down()
|
|
|
|
def stepUpOctave():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("stepUpOctave")
|
|
else:
|
|
_applyToItem("stepUpOctaveNoteNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
upOctave()
|
|
|
|
def stepDownOctave():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("stepDownOctave")
|
|
else:
|
|
_applyToItem("stepDownOctaveNoteNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
downOctave()
|
|
|
|
def transpose(rootPitch, targetPitch):
|
|
"""For the cursor position. Based on automatic transpose. The interval is caculated from
|
|
two pitches.
|
|
There is also tranposeScore. But no transposeTrack, this is just select track and transpose."""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("intervalAutomatic", parameterListForEachSelectedItem = [[rootPitch, targetPitch]])
|
|
else:
|
|
_applyToItem("intervalAutomatic", parameters = [rootPitch, targetPitch])
|
|
#no cursor movement.
|
|
|
|
|
|
|
|
def toggleBeam():
|
|
"""Place a beam at each end of the selection if all durations are <= D8"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
validSelection, topLeftCursor, bottomRightCursor, listOfChangedTrackIds, *selectedTracksAndItems = session.data.listOfSelectedItems(removeContentLinkedData = True)
|
|
if validSelection and len(listOfChangedTrackIds) == 1 :
|
|
#a valid selection has >=2 items.
|
|
filteredList = [item for item, props in selectedTracksAndItems[0] if type(item) in (items.Chord, items.Rest)] #only music items of the only track, which are themselves tuples (item, propertiesLikeKeysigs)
|
|
#oneDur = filteredList[0].durationGroup.baseDuration
|
|
#correctDurations = oneDur <= D8 and all(item.durationGroup.baseDuration == oneDur for item in filteredList)
|
|
correctDurations = all(item.durationGroup.baseDuration <= D8 for item in filteredList)
|
|
if correctDurations:
|
|
first = filteredList[0]
|
|
last = filteredList[-1]
|
|
if type(first) is items.Chord and type(last) is items.Chord:
|
|
removeBeam() #clean up before setting a new boundary
|
|
first.toggleBeam()
|
|
last.toggleBeam()
|
|
callbacks._updateTrack(*listOfChangedTrackIds)
|
|
return True
|
|
|
|
return False #all branches except the only True one end here.
|
|
else:
|
|
_applyToItem("toggleBeam")
|
|
|
|
def removeBeam():
|
|
"""Get rid of all beams in the selection. This is meant as clean-up tool for the user
|
|
but also utilized in toggleBeam"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("removeBeam")
|
|
else:
|
|
_applyToItem("removeBeam")
|
|
|
|
#Rests
|
|
def insertRest(baseDuration):
|
|
duration = items.Duration(baseDuration)
|
|
#duration.dots = session.data.cursor.getPrevailingDot()
|
|
duration.dots = 0
|
|
|
|
#audio Feedback?
|
|
rest = items.Rest(duration)
|
|
insertItem(rest)
|
|
|
|
def insertRest1():
|
|
insertRest(D1)
|
|
def insertRest2():
|
|
insertRest(D2)
|
|
def insertRest4():
|
|
insertRest(D4)
|
|
def insertRest8():
|
|
insertRest(D8)
|
|
def insertRest16():
|
|
insertRest(D16)
|
|
def insertRest32():
|
|
insertRest(D32)
|
|
def insertRest64():
|
|
insertRest(D64)
|
|
def insertRest128():
|
|
insertRest(D128)
|
|
|
|
def insertMultiMeasureRest(numberOfMeasures):
|
|
"""MultiMeasureRests get calculated on export"""
|
|
assert int(numberOfMeasures) == numberOfMeasures and numberOfMeasures > 0
|
|
if session.data.currentTrack().state.metricalInstruction().oneMeasureInTicks > 0:
|
|
insertItem(items.MultiMeasureRest(numberOfMeasures))
|
|
|
|
#Clefs
|
|
def insertClef(clefString):
|
|
clef = items.Clef(clefString)
|
|
insertItem(clef)
|
|
|
|
#Slurs
|
|
def insertLegatoSlur():
|
|
"""no open and close. The context and order determines what
|
|
type of slur we insert, even export. Dynamically adjusts"""
|
|
slur = items.LegatoSlur()
|
|
insertItem(slur)
|
|
|
|
|
|
#Key Signatures
|
|
def insertKeySignature(root, scheme):
|
|
keysig = items.KeySignature(pitchmath.plain(root), scheme)
|
|
insertItem(keysig)
|
|
|
|
def commonKeySignaturesAsList():
|
|
"""For the GUIs convenience so they can populate their
|
|
drop down lists.
|
|
Yes, we could use the dict from insertCommonKeySignature but
|
|
we want a special order which is not the same as the alphabetical.
|
|
So a bit of redundancy is ok."""
|
|
return [
|
|
"Major",
|
|
"Minor",
|
|
"Dorian",
|
|
"Phrygian",
|
|
"Lydian",
|
|
"Mixolydian",
|
|
"Locrian",
|
|
"Hollywood",
|
|
]
|
|
|
|
def insertCommonKeySignature(root, scheme):
|
|
"""example:
|
|
insertCommonKeySignature(P_C, "Major")
|
|
"""
|
|
schemes = {
|
|
"Major": [0,0,0,0,0,0,0],
|
|
"Minor": [0,0,-10,0,0,-10,-10],
|
|
"Dorian": [0,0,-10,0,0,0,-10],
|
|
"Phrygian": [0,-10,-10,0,0,-10,-10],
|
|
"Lydian": [0,0,0,+10,0,0,0],
|
|
"Mixolydian": [0,0,0,0,0,0,-10],
|
|
"Locrian": [0,-10,-10,0,-10,-10,-10],
|
|
"Hollywood": [0,0,0,0,0,-10,-10], #The "Hollywood"-Scale. Stargate, Lord of the Rings etc.
|
|
#TODO: MOAR!
|
|
}
|
|
insertKeySignature(root, schemes[scheme])
|
|
|
|
def insertCursorCommonKeySignature(scheme):
|
|
"""Root note is generated from the cursor pitch position and takes the previous keysig
|
|
into account."""
|
|
root = pitchmath.plain(session.data.cursor.pitchindex)
|
|
|
|
keysig = session.data.currentTrack().state.keySignature()
|
|
pitchToInsert = pitchmath.toScale(session.data.cursor.pitchindex, keysig)
|
|
|
|
insertCommonKeySignature(pitchToInsert, scheme)
|
|
|
|
|
|
|
|
#def insertTimeSignature(demoniator, nominator): #upper/lower.
|
|
# """example:
|
|
# insertTimeSignature(4, 210*2**8) #4/4
|
|
# """
|
|
# insertItem(items.TimeSignature(demoniator, nominator))
|
|
|
|
|
|
#Metrical Instructions
|
|
def insertMetricalInstruction(treeOfInstructions, lilypondOverride = None):
|
|
item = items.MetricalInstruction(treeOfInstructions)
|
|
if lilypondOverride:
|
|
item.lilypondParameters["override"] = lilypondOverride
|
|
insertItem(item)
|
|
|
|
def commonMetricalInstructionsAsList():
|
|
"""for musical reasons 5/4 and 7/8 and other a-symetrical
|
|
metrical instructions cannot be in here since it is unknown which
|
|
internal version the user wants."""
|
|
return [
|
|
"off",
|
|
"4/4",
|
|
"3/4",
|
|
"6/8",
|
|
"2/4",
|
|
"12/8",
|
|
"6/4",
|
|
"3/8",
|
|
"1/1",
|
|
]
|
|
|
|
def insertCommonMetricalInstrucions(scheme):
|
|
"""A metrical instruction requires a lilypond override. You can use them without of course but
|
|
they will not work when exported."""
|
|
schemes = {
|
|
"off" : (),
|
|
"2/4" : (D4, D4),
|
|
"3/4" : (D4, D4, D4),
|
|
"4/4" : ((D4, D4), (D4, D4)),
|
|
"6/8" : (int(D4*1.5),int(D4*1.5)), #aka 2/4 with triplet sub-level
|
|
"12/8" : ((D8, D8, D8), (D8, D8, D8), (D8, D8, D8), (D8, D8, D8)), #aka 4/4 with triplet sub-level
|
|
"6/4" : ((D4, D4, D4),(D4, D4, D4)),
|
|
"3/8" : (D8, D8, D8),
|
|
"1/1" : (D1,),
|
|
}
|
|
lilypond = {
|
|
"off" : "\\mark \"X\" \\cadenzaOn ",
|
|
"2/4" : "\\cadenzaOff \\time 2/4",
|
|
"3/4" : "\\cadenzaOff \\time 3/4",
|
|
"4/4" : "\\cadenzaOff \\time 4/4",
|
|
"6/8" : "\\cadenzaOff \\time 6/8",
|
|
"12/8" : "\\cadenzaOff \\time 12/8",
|
|
"6/4" : "\\cadenzaOff \\time 6/4",
|
|
"3/8" : "\\cadenzaOff \\time 3/8",
|
|
"1/1" : "\\cadenzaOff \\time 1/1",
|
|
}
|
|
insertMetricalInstruction(schemes[scheme], lilypondOverride = lilypond[scheme])
|
|
|
|
#Velocity and Dynamic Signatures
|
|
def insertDynamicSignature(keyword):
|
|
dynSig = items.DynamicSignature(keyword)
|
|
insertItem(dynSig)
|
|
|
|
def insertDynamicPiano():
|
|
insertDynamicSignature("p")
|
|
|
|
def insertDynamicForte():
|
|
insertDynamicSignature("f")
|
|
|
|
def insertDynamicTacet():
|
|
insertDynamicSignature("tacet")
|
|
|
|
def insertDynamicRamp():
|
|
"""Crescendo or Decrescendo. Determined automatically"""
|
|
dynRamp = items.DynamicRamp()
|
|
insertItem(dynRamp)
|
|
|
|
|
|
def setNoteDynamic(noteId, value):
|
|
listOfTracks, note = session.data.noteById(noteId)
|
|
session.history.register(lambda nId=noteId, v=note.dynamic.velocityModification: setNoteDynamic(nId, v), descriptionString = "set note dynamic")
|
|
#todo: better undo?
|
|
note.dynamic.velocityModification = value
|
|
for track in listOfTracks:
|
|
callbacks._updateTrack(id(track))
|
|
|
|
def clearNoteDynamic(noteId):
|
|
listOfTracks, note = session.data.noteById(noteId)
|
|
session.history.register(lambda nId=noteId, v=note.dynamic.velocityModification: setNoteDynamic(nId, v), descriptionString = "clear note dynamic")
|
|
#todo: better undo?
|
|
note.dynamic.velocityModification = 0
|
|
for track in listOfTracks:
|
|
callbacks._updateTrack(id(track))
|
|
|
|
def setNoteMods(noteId, valueOn, valueOff):
|
|
"""
|
|
value is a float that is added as percentage of the original value
|
|
(without mods)
|
|
|
|
Is used for a specific note, not the cursor position.
|
|
"""
|
|
listOfTracks, note = session.data.noteById(noteId)
|
|
session.history.register(lambda nId=noteId, on=note.duration.shiftStart, off=note.duration.shiftEnd: setNoteMods(nId, on, off), descriptionString = "fine tune note duration")
|
|
#todo: better undo?
|
|
note.duration.shiftStart = valueOn
|
|
note.duration.shiftEnd = valueOff
|
|
#we can't use callbacks._updateChangedTracks() here because that depends on the cursor position and checks for linked content for the currentBlock. the note can be anywhere
|
|
for track in listOfTracks:
|
|
callbacks._updateTrack(id(track))
|
|
|
|
def clearNoteMod(noteId):
|
|
"""see setNoteMods"""
|
|
listOfTracks, note = session.data.noteById(noteId)
|
|
session.history.register(lambda nId=noteId, on=note.duration.shiftStart, off=note.duration.shiftEnd: setNoteMods(nId, on, off), descriptionString = "clear note duration fine tuning")
|
|
#todo: better undo?
|
|
note.duration.shiftStart = 0
|
|
note.duration.shiftEnd = 0
|
|
for track in listOfTracks:
|
|
callbacks._updateTrack(id(track))
|
|
|
|
|
|
#Cursor Variants of the above
|
|
def moreVelocity():
|
|
"""Increase the relative velocity modficator for the note
|
|
nearest to the cursor. With selection/single support."""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("moreVelocity")
|
|
else:
|
|
_applyToItem("moreVelocityNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def lessVelocity():
|
|
"""Opposite of moreVelocity"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("lessVelocity")
|
|
else:
|
|
_applyToItem("lessVelocityNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def moreDuration():
|
|
"""Increase the duration modficator for the note
|
|
nearest to the cursor. With selection/single support.
|
|
For simplicity reasons this only shifts the right end, the ending,
|
|
of the note. The beginning needs the full command setNoteMods"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("moreDuration")
|
|
else:
|
|
_applyToItem("moreDurationNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def lessDuration():
|
|
"""Opposite of moreDuration. But still for the ending of the note,
|
|
not the beginning"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("lessDuration")
|
|
else:
|
|
_applyToItem("lessDurationNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
def resetDurationVelocity():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("resetVelocityAndDurationMods")
|
|
else:
|
|
_applyToItem("resetVelocityAndDurationModsNearPitch", parameters = [session.data.cursor.pitchindex])
|
|
|
|
#Graphs. Tempo and CC
|
|
def getListOfGraphInterpolationTypesAsStrings():
|
|
return ["linear", "standalone"]
|
|
|
|
|
|
#Control Change Subtracks
|
|
def _lazyCCUndoRedo(trId:int, cc:int, data, description:str): #For items and blocks, not for tracks.
|
|
"""
|
|
Lazy Undo for CCs deals with one CC in one Track at a time. No blocks are involved.
|
|
|
|
The CC tracks are not full of data. We can afford it to use the save/load system to
|
|
store complete states instead of incremental changes.
|
|
"""
|
|
track = session.data.trackById(trId)
|
|
oldData = track.ccGraphTracks[cc].serialize()
|
|
track.ccGraphTracks[cc] = GraphTrackCC.instanceFromSerializedData(data, parentTrack=track)
|
|
session.history.register(lambda: _lazyCCUndoRedo(trId, cc, oldData, description), descriptionString=description)
|
|
callbacks._historyChanged()
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def _CCUndoCreater(trId:int, cc:int, description:str=""): #For items and blocks, not for tracks.
|
|
"""Can be used whenever saving the old state and actually registering undo does not depend
|
|
on the success of a function. In other words: if you could write all undo related lines
|
|
in a row, use this instead."""
|
|
track = session.data.trackById(trId)
|
|
oldData = track.ccGraphTracks[cc].serialize()
|
|
session.history.register(lambda: _lazyCCUndoRedo(trId, cc, oldData, description), descriptionString=description)
|
|
callbacks._historyChanged()
|
|
|
|
|
|
##CC Tracks
|
|
def newGraphTrackCC(trId, cc):
|
|
"""add a new CC Path for CC to the given Track.
|
|
Do nothing if already existent."""
|
|
track = session.data.trackById(trId)
|
|
track.newGraphTrackCC(cc)
|
|
session.history.clear()
|
|
callbacks._graphCCTracksChanged(trId) #create structure: all CC graphs, accessed by CC number (0-127)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def _addExistingGraphTrackCC(trId, cc, graphTrackObject):
|
|
"""For undo deleteGraphTrackCC"""
|
|
track = session.data.trackById(trId)
|
|
track.addExistingGraphTrackCC(cc, graphTrackObject)
|
|
session.history.clear()
|
|
callbacks._graphCCTracksChanged(trId) #create structure: all CC graphs, accessed by CC number (0-127)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def deleteGraphTrackCC(trId, cc):
|
|
track = session.data.trackById(trId)
|
|
deletedGraphTrackObject = track.deleteGraphTrackCC(cc)
|
|
session.history.clear()
|
|
callbacks._graphCCTracksChanged(trId) #this should delete any GUI cc track
|
|
|
|
## CC Blocks
|
|
|
|
def changeGraphBlockDuration(graphBlockId, newDurationInTicks):
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
_CCUndoCreater(trId, cc, "CC Block Duration")
|
|
graphBlock.duration = newDurationInTicks
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def appendGraphBlock(trId, cc):
|
|
"""Append a small duration block to the current cc track"""
|
|
ccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
_CCUndoCreater(trId, cc, "Append CC Block")
|
|
newBlock = ccTrack.appendGraphBlock()
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def rearrangeCCBlocks(trId, cc, listOfBlockIds):
|
|
ccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
_CCUndoCreater(trId, cc, "Move CC Block")
|
|
ccTrack.rearrangeBlocks(listOfBlockIds)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def moveCCBlockToOtherTrack(graphBlockId, newTrackId, listOfBlockIdsForNewTrack):
|
|
"""Modified copy of api.moveBlockToOtherTrack.
|
|
This only moves to the same CC value. not from volume to modwheel."""
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
oldccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
newGraphTrack = session.data.trackById(newTrackId).ccGraphTracks[cc] #we assume this track has this CC already activated.
|
|
|
|
if len(oldccTrack.blocks) == 1:
|
|
return False #it is not possible to move the only block.
|
|
|
|
with session.history.sequence("Move CC to other track"):
|
|
_CCUndoCreater(trId, cc, "MoveToOther Old")
|
|
_CCUndoCreater(newTrackId, cc, "MoveToOther New")
|
|
callbacks._historyChanged()
|
|
|
|
newGraphTrack.appendExistingGraphBlock(graphBlock)
|
|
newGraphTrack.rearrangeBlocks(listOfBlockIdsForNewTrack)
|
|
oldccTrack.deleteBlock(graphBlock) #It is important that we delete the block at exactly this point in time, not ealier. Otherwise the reference for undo will go away.
|
|
graphBlock.parentGraphTrack = newGraphTrack
|
|
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
callbacks._updateGraphTrackCC(newTrackId, cc) #in case of a linked block this is redundant, but for a normal move it is not redundant.
|
|
|
|
def changeCCBlock(graphBlockId, newParametersDict):
|
|
"""Mostly Duration changes. But includes the name as well"""
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
_CCUndoCreater(trId, cc, "Change CC Block")
|
|
graphBlock.putDataFromDict(newParametersDict)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def duplicateCCBlock(graphBlockId, times = 1):
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
ccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
_CCUndoCreater(trId, cc, "Duplicate CC Block")
|
|
for i in range(times):
|
|
ccTrack.duplicateBlock(graphBlock)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def duplicateContentLinkCCBlock(graphBlockId, times = 1):
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
ccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
_CCUndoCreater(trId, cc, "Content Link CC Block")
|
|
for i in range(times):
|
|
ccTrack.duplicateContentLinkBlock(graphBlock)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def unlinkCCBlock(graphBlockId):
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
_CCUndoCreater(trId, cc, "Unlink CC Block")
|
|
newData, newLinkedContentBlocks, newDuration = graphBlock.getUnlinkedData()
|
|
assert newData, newData
|
|
assert newDuration, newDuration
|
|
graphBlock.data = newData
|
|
graphBlock.linkedContentBlocks = newLinkedContentBlocks
|
|
graphBlock._duration = newDuration
|
|
|
|
def deleteCCBlock(graphBlockId):
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
ccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
oldData = ccTrack.serialize()
|
|
deletedBlock = ccTrack.deleteBlock(graphBlock)
|
|
if deletedBlock: #not the last block
|
|
#Blocks are never truly deleted but stored in the GraphBlock.allBlocks dict. This keeps the reference to this deleted block alive and it can be added through rearrange, which gets its blocks from this dict.
|
|
description = "Delete CC Block"
|
|
session.history.register(lambda: _lazyCCUndoRedo(trId, cc, oldData, description), descriptionString=description)
|
|
callbacks._historyChanged()
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def extendLastCCBlockToTrackLength(trId, cc):
|
|
ccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
lastCCBlock = ccTrack.blocks[-1]
|
|
_CCUndoCreater(trId, cc, "Extend last CC Block to Track length")
|
|
lastCCBlock.extendToTrackLength(ccTrack)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def splitCCBlock(graphBlockId, positionInTicksRelativeToBlock):
|
|
"""tick position is relative to block start"""
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
ccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
oldData = ccTrack.serialize()
|
|
success = ccTrack.splitGraphBlock(graphBlock, positionInTicksRelativeToBlock)
|
|
if success:
|
|
description = "Split CC Block"
|
|
session.history.register(lambda: _lazyCCUndoRedo(trId, cc, oldData, description), descriptionString=description)
|
|
callbacks._historyChanged()
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def mergeWithNextGraphBlock(graphBlockId):
|
|
trId, cc, graphBlock = session.data.graphBlockById(graphBlockId)
|
|
ccTrack = session.data.trackById(trId).ccGraphTracks[cc]
|
|
oldData = ccTrack.serialize()
|
|
positionForSplit = ccTrack.mergeWithNextGraphBlock(graphBlock)
|
|
if positionForSplit:
|
|
description = "Split CC Block"
|
|
session.history.register(lambda: _lazyCCUndoRedo(trId, cc, oldData, description), descriptionString=description)
|
|
callbacks._historyChanged()
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
##CC Blocks and User Points
|
|
|
|
def addGraphItem(blockId, positionInTicksRelativeToBlock, newCCValue):
|
|
"""blockId includes the track as well as the CC"""
|
|
trId, cc, graphBlock = session.data.graphBlockById(blockId)
|
|
graphItem = GraphItem(newCCValue)
|
|
_addExistingGraphItem(blockId, positionInTicksRelativeToBlock, graphItem)
|
|
|
|
def _addExistingGraphItem(blockId, positionInTicksRelativeToBlock, graphItem):
|
|
"""blockId includes the track as well as the CC"""
|
|
trId, cc, graphBlock = session.data.graphBlockById(blockId)
|
|
_CCUndoCreater(trId, cc, "Add CC Point")
|
|
graphBlock.insert(graphItem, positionInTicksRelativeToBlock)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def removeGraphItem(graphItemId):
|
|
trId, cc, graphBlock, graphItem = session.data.graphItemById(graphItemId)
|
|
_CCUndoCreater(trId, cc, "Remove CC Point")
|
|
tickPositionRelativeToBlockStart = graphBlock.find(graphItem)
|
|
graphBlock.remove(tickPositionRelativeToBlockStart)
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def changeGraphItem(graphItemId, moveInTicks, newCCValue):
|
|
trId, cc, graphBlock, graphItem = session.data.graphItemById(graphItemId)
|
|
_CCUndoCreater(trId, cc, "Change CC Point")
|
|
currentTickPositionRelativeToBlock = graphBlock.find(graphItem)
|
|
graphBlock.move(currentTickPositionRelativeToBlock, currentTickPositionRelativeToBlock + moveInTicks)
|
|
graphItem.ccStart = newCCValue
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
def changeGraphItemInterpolation(graphItemId, graphType):
|
|
"""graphType is "linear" or "standalone" """
|
|
trId, cc, graphBlock, graphItem = session.data.graphItemById(graphItemId)
|
|
_CCUndoCreater(trId, cc, "CC Point Interpolation")
|
|
graphItem.graphType = graphType
|
|
callbacks._updateGraphTrackCC(trId, cc)
|
|
|
|
|
|
#Tempo Track
|
|
|
|
def _lazyTempoTrackUndoRedo(new, description:str):
|
|
"""The tempo track is not full of data. We can afford it to use the save/load system to
|
|
store complete states instead of incremental changes."""
|
|
old = session.data.tempoTrack.serialize()
|
|
session.data.tempoTrack = TempoTrack.instanceFromSerializedData(new, parentData=session.data)
|
|
session.history.register(lambda d=old: _lazyTempoTrackUndoRedo(d, description), descriptionString=description)
|
|
callbacks._historyChanged()
|
|
callbacks._updateTempoTrack()
|
|
|
|
def _tempoTrackUndoCreater(description:str):
|
|
"""Can be used whenever saving the old state and actually registering undo does not depend
|
|
on the success of a function. In other words: if you could write all undo related lines
|
|
in a row, use this instead."""
|
|
old = session.data.tempoTrack.serialize()
|
|
session.history.register(lambda d=old: _lazyTempoTrackUndoRedo(d, description), descriptionString=description)
|
|
callbacks._historyChanged()
|
|
|
|
def addTempoItem(blockId, positionInTicksRelativeToBlock, unitsPerMinute, referenceTicks, graphType = "standalone"):
|
|
"""blockId includes the track as well as the CC"""
|
|
tempoItem = TempoItem(unitsPerMinute, referenceTicks)
|
|
tempoItem.graphType = graphType
|
|
_addExistingTempoItem(blockId, positionInTicksRelativeToBlock, tempoItem) #handles undo and callbacks
|
|
return tempoItem
|
|
|
|
def _addExistingTempoItem(blockId, positionInTicksRelativeToBlock, tempoItem):
|
|
_tempoTrackUndoCreater("Add Tempo Change")
|
|
tempoBlock = session.data.tempoTrack.tempoBlockById(blockId)
|
|
tempoBlock.insert(tempoItem, positionInTicksRelativeToBlock)
|
|
callbacks._updateTempoTrack()
|
|
|
|
def removeTempoItem(tempoItemId):
|
|
_tempoTrackUndoCreater("Delete Tempo Change")
|
|
tempoBlock, tempoItem = session.data.tempoTrack.tempoItemById(tempoItemId)
|
|
tickPositionRelativeToBlockStart = tempoBlock.find(tempoItem)
|
|
tempoBlock.remove(tickPositionRelativeToBlockStart)
|
|
callbacks._updateTempoTrack()
|
|
|
|
|
|
def moveTempoItem(tempoItemId, tickPositionAbsolute):
|
|
"""Figures out the target block automatically"""
|
|
blockId, blockPosition = session.data.tempoTrack.tempoBlocKByAbsolutePosition(tickPositionAbsolute)
|
|
new_positionInTicksRelativeToBlock = tickPositionAbsolute - blockPosition
|
|
tempoBlock, tempoItem = session.data.tempoTrack.tempoItemById(tempoItemId)
|
|
tempoBlock.remove(tempoBlock.find(tempoItem))
|
|
_addExistingTempoItem(blockId, new_positionInTicksRelativeToBlock, tempoItem)
|
|
|
|
def removeCurrentTempoItem():
|
|
"""remove the tempo item which is at the tickindex of the cursor or left of it"""
|
|
tempoItem = session.data.tempoTrack.tempoAtTickPosition(session.data.currentTrack().state.tickindex) #This returns the leftmost item of the tick position
|
|
removeTempoItem(id(tempoItem)) #undo and callback
|
|
|
|
def changeTempoBlockDuration(tempoBlockId, newDurationInTicks):
|
|
_tempoTrackUndoCreater("Tempo Block Duration")
|
|
tempoBlock = session.data.tempoTrack.tempoBlockById(tempoBlockId)
|
|
tempoBlock.duration = newDurationInTicks
|
|
callbacks._updateTempoTrack()
|
|
|
|
def rearrangeTempoBlocks(listOfBlockIds):
|
|
_tempoTrackUndoCreater("Move Tempo Block")
|
|
session.data.tempoTrack.rearrangeBlocks(listOfBlockIds)
|
|
callbacks._updateTempoTrack()
|
|
|
|
def appendTempoBlock():
|
|
_tempoTrackUndoCreater("Append Tempo Block")
|
|
newBlock = session.data.tempoTrack.appendTempoBlock()
|
|
callbacks._updateTempoTrack()
|
|
|
|
def duplicateTempoBlock(tempoBlockId, times = 1):
|
|
_tempoTrackUndoCreater("Duplicate Tempo Block")
|
|
tempoBlock = session.data.tempoTrack.tempoBlockById(tempoBlockId)
|
|
for i in range(times):
|
|
session.data.tempoTrack.duplicateBlock(tempoBlock)
|
|
callbacks._updateTempoTrack()
|
|
|
|
def duplicateContentLinkTempoBlock(tempoBlockId, times = 1):
|
|
"""This is also create content link"""
|
|
_tempoTrackUndoCreater("Content Link Tempo Block")
|
|
tempoBlock = session.data.tempoTrack.tempoBlockById(tempoBlockId)
|
|
for i in range(times):
|
|
session.data.tempoTrack.duplicateContentLinkBlock(tempoBlock)
|
|
callbacks._updateTempoTrack()
|
|
|
|
|
|
def changeTempoBlock(tempoBlockId, newParametersDict):
|
|
_tempoTrackUndoCreater("Change Tempo Block")
|
|
tempoBlock = session.data.tempoTrack.tempoBlockById(tempoBlockId)
|
|
tempoBlock.putDataFromDict(newParametersDict)
|
|
callbacks._updateTempoTrack()
|
|
|
|
def unlinkTempoBlock(tempoBlockId):
|
|
tempoBlock = session.data.tempoTrack.tempoBlockById(tempoBlockId)
|
|
if len(tempoBlock.linkedContentBlocks) == 1:
|
|
return #This is not a content link block
|
|
|
|
_tempoTrackUndoCreater("Unlink Tempo Block")
|
|
|
|
newData, newLinkedContentBlocks, newDuration = tempoBlock.getUnlinkedData() #does not set itself, just returns
|
|
tempoBlock.data = newData
|
|
tempoBlock.linkedContentBlocks.remove(tempoBlock) #the block still exists, so WeakRef will not remove it.
|
|
tempoBlock.linkedContentBlocks = newLinkedContentBlocks
|
|
tempoBlock._duration = newDuration #mutable list of length 1
|
|
callbacks._updateTempoTrack()
|
|
|
|
|
|
def splitTempoBlock(tempoBlockId, positionInTicksRelativeToBlock:int):
|
|
"""tick position is relative to block start"""
|
|
old = session.data.tempoTrack.serialize()
|
|
success = session.data.tempoTrack.splitTempoBlock(tempoBlockId, positionInTicksRelativeToBlock)
|
|
if success:
|
|
session.history.register(lambda d=old, desc="Split Tempo Block": _lazyTempoTrackUndoRedo(d, desc), descriptionString="Split Tempo Block")
|
|
callbacks._updateTempoTrack()
|
|
|
|
def mergeWithNextTempoBlock(tempoBlockId):
|
|
old = session.data.tempoTrack.serialize()
|
|
positionForSplit = session.data.tempoTrack.mergeWithNextTempoBlock(tempoBlockId)
|
|
if positionForSplit:
|
|
session.history.register(lambda d=old, desc="Join Tempo Block": _lazyTempoTrackUndoRedo(d, desc), descriptionString="Join Tempo Block")
|
|
callbacks._historyChanged()
|
|
callbacks._updateTempoTrack()
|
|
|
|
def deleteTempoBlock(tempoBlockId):
|
|
old = session.data.tempoTrack.serialize()
|
|
tempoBlock = session.data.tempoTrack.tempoBlockById(tempoBlockId)
|
|
deletedBlock = session.data.tempoTrack.deleteBlock(tempoBlock)
|
|
if deletedBlock:
|
|
session.history.register(lambda d=old, desc="Delete Tempo Block": _lazyTempoTrackUndoRedo(d, desc), descriptionString="Delete Tempo Block")
|
|
callbacks._updateTempoTrack()
|
|
|
|
def insertTempoItemAtAbsolutePosition(tickPositionAbsolute, unitsPerMinute, referenceTicks, graphType):
|
|
blockId, blockPosition = session.data.tempoTrack.tempoBlocKByAbsolutePosition(tickPositionAbsolute)
|
|
positionInTicksRelativeToBlock = tickPositionAbsolute - blockPosition
|
|
addTempoItem(blockId, positionInTicksRelativeToBlock, unitsPerMinute, referenceTicks, graphType)
|
|
|
|
def tempoAtTickPosition(tick):
|
|
tempoItem = session.data.tempoTrack.tempoAtTickPosition(tick)
|
|
return tempoItem.unitsPerMinute, tempoItem.referenceTicks
|
|
|
|
def insertTempoChangeDuringDuration(percentageUnitsPerMinuteAsFloat):
|
|
"""Instead of a fermata this sets a tempo change which start- and end-point are derived
|
|
from the current items duration
|
|
|
|
To enable the user to compare the tempo values we keep the original referenceTicks.
|
|
The new units per minute value is given as percentage parameter. The reasoning behind that is
|
|
that the user should think 'I want that duration to be double as long as normal = half tempo = 0.5' """
|
|
|
|
if percentageUnitsPerMinuteAsFloat <= 0:
|
|
raise ValueError("Give a float value bigger than zero. 1.0 is no modification. 0.5 is half tempo, 2.0 is double tempo")
|
|
|
|
curItem = session.data.currentItem()
|
|
if curItem: # not appending(None)
|
|
dur = curItem.logicalDuration()
|
|
if not dur: #not a clef
|
|
return None
|
|
startTick = session.data.currentTrack().state.tickindex
|
|
endTick = startTick + dur #we don't need to worry about the next note starting on that value or not. midi tempo changes can happen between note on and note off.
|
|
tempoItem = session.data.tempoTrack.tempoAtTickPosition(startTick)
|
|
#originalUnitsPerMinute, originalReferenceTicks = session.data.tempoTrack.tempoAtTickPosition(startTick)
|
|
newUnitsPerMinute = tempoItem.unitsPerMinute * percentageUnitsPerMinuteAsFloat
|
|
insertTempoItemAtAbsolutePosition(startTick, newUnitsPerMinute, tempoItem.referenceTicks, graphType = "standalone")
|
|
insertTempoItemAtAbsolutePosition(endTick, tempoItem.unitsPerMinute, tempoItem.referenceTicks, graphType = "standalone")
|
|
|
|
def currentTempoScalingFactor():
|
|
return session.data.tempoTrack.factor
|
|
|
|
def changeTempoScaling(factor:float):
|
|
"""The factor is always a factor from x1, not from the previous
|
|
value"""
|
|
session.data.tempoTrack.setFactor(float(factor))
|
|
callbacks._tempoScalingChanged(session.data.tempoTrack.factor)
|
|
|
|
#Toolbox
|
|
#High Level commands
|
|
|
|
def pedalNotes(pedalDuration):
|
|
"""Split the chord so that it consists of all *pedalDuration*s and use the cursor as a pitch
|
|
for all but the first, which keeps its original notelist.
|
|
|
|
Since the cursor note is in the current keysig only it will be used even if applied to multiple
|
|
tracks. E.g. treble clef middle line in Track 1 has F-Major and will input a Bes but if Track 2
|
|
is in C-Major it will still use Bes as the pedal note, and not B.
|
|
|
|
This is a musically unlikely corner case so we accept the inaccuracy.
|
|
"""
|
|
#TODO: To iron out the corner case in the docstring we would have to look at each notes cached keysig.
|
|
def replaceWithPedalNotes(self):
|
|
if not type(self) == items.Chord:
|
|
return False
|
|
|
|
newParts = int(self.durationGroup.baseDuration / pedalDuration)
|
|
if not newParts >= 2:
|
|
return False
|
|
originalPitches = self.notelist #keep the actual list, not only the contents
|
|
keysig = session.data.currentTrack().state.keySignature()
|
|
pitchToInsert = pitchmath.toScale(session.data.cursor.pitchindex, keysig)
|
|
pedalNote = self.notelist[0].copy(self)
|
|
pedalNote.pitch = pitchToInsert
|
|
self.notelist = [pedalNote]
|
|
undoSplit = self.split(newParts) #split uses the temporary note
|
|
targetDuration = self.durationGroup.minimumNote.duration.copy()
|
|
|
|
#Restore the first note to original pitch, but keep the new duration, at least in the mininumNote
|
|
self.notelist = originalPitches
|
|
self.durationGroup.cacheMinimumNote()
|
|
#self.notelist[self.notelist.index(self.durationGroup.minimumNote)] =
|
|
self.durationGroup.minimumNote.duration = targetDuration
|
|
self.durationGroup.cacheMinimumNote()
|
|
return undoSplit
|
|
|
|
items.Item._replaceWithPedalNotes = replaceWithPedalNotes #this happens each time api.pedalNotes gets called, which is good because the "duration" parameter changes each time.
|
|
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("_replaceWithPedalNotes")
|
|
else:
|
|
_applyToItem("_replaceWithPedalNotes")
|
|
|
|
|
|
def mirrorAroundCursor():
|
|
if session.data.cursorWhenSelectionStarted:
|
|
_applyToSelection("mirrorAroundCursor", parameterListForEachSelectedItem = [[session.data.cursor.pitchindex]])
|
|
else:
|
|
_applyToItem("mirrorAroundCursor", parameters = [session.data.cursor.pitchindex])
|
|
|
|
#All the random inserts work on prevailing duration.
|
|
def _randomPitch(lowest, highest, mode = "inScale"):
|
|
"""
|
|
returns a pitch integer
|
|
|
|
Does depend on the track state and cursor. This is a function that is supposed to be called (indirectly)
|
|
by the user and not suited for a script where the cursor stays at the beginning.
|
|
|
|
lowest is a pitch value like 1420
|
|
Modes:
|
|
-1 = full spectrum, reserved for microintervals
|
|
0 = enharmonic spectrum.
|
|
1 = without double-b and x
|
|
2 = in Scale, default"""
|
|
|
|
note = random.randrange(lowest, highest, 10) #step-size 10
|
|
keysig = session.data.currentTrack().state.keySignature()
|
|
if mode == "inScale":
|
|
return pitchmath.toScale(note, keysig)
|
|
elif mode == "chromatic":
|
|
#A conversion to midi is just an abstract frequency and back to the current keysig
|
|
return pitchmath.fromMidi(pitchmath.toMidi[note], keysig)
|
|
elif mode == "enharmonic":
|
|
return note
|
|
elif mode == "microintervals":
|
|
raise NotImplementedError
|
|
else:
|
|
raise ValueError("Mode unknown: {}".format(mode))
|
|
|
|
def insertRandomChromaticInClefRange():
|
|
clef = session.data.currentTrack().state.clef()
|
|
insertChord(session.data.cursor.prevailingBaseDuration, _randomPitch(clef.rangeLowest, clef.rangeHighest, mode="chromatic"))
|
|
|
|
def insertRandomFromScaleInClefRange():
|
|
clef = session.data.currentTrack().state.clef()
|
|
insertChord(session.data.cursor.prevailingBaseDuration, _randomPitch(clef.rangeLowest, clef.rangeHighest))
|
|
|
|
def insertRandomFromScaleAuthenticModeCursor():
|
|
insertChord(session.data.cursor.prevailingBaseDuration, _randomPitch(session.data.cursor.pitchindex, session.data.cursor.pitchindex + 350, mode = "inScale"))
|
|
|
|
def insertRandomFromScaleHypoModeCursor():
|
|
insertChord(session.data.cursor.prevailingBaseDuration, _randomPitch(session.data.cursor.pitchindex - 3*50, session.data.cursor.pitchindex + 4*50, mode = "inScale"))
|
|
|
|
def insertRandomFromClipboard():
|
|
"""Only the pitch, not the duration"""
|
|
#TODO: After real world testing decide if this works with the clipboard or if we need a separate buffer
|
|
#TODO currently the pool is created each time a note is requested. This could easily moved into the copy function.
|
|
if session.data.copyObjectsBuffer:
|
|
pool = set(flatList(session.data.copyObjectsBuffer)) #set to remove content links
|
|
pool = [i for i in pool if type(i) is items.Chord]
|
|
if pool: #all chords
|
|
item = items.createChordOrRest(session.data.cursor.prevailingBaseDuration, random.choice(pool).copy().pitchlist())
|
|
insertItem(item)
|
|
|
|
#Ordering
|
|
def _listOChordsFromSelection():
|
|
"""Returns (None, None) or a tuple(listOfChangedTrackIds, list of notelists (per track))"""
|
|
if session.data.cursorWhenSelectionStarted:
|
|
validSelection, topLeftCursor, bottomRightCursor, listOfChangedTrackIds, *selectedTracksAndItems = session.data.listOfSelectedItems(removeContentLinkedData=True)
|
|
if not validSelection:
|
|
return None, None
|
|
chordlists = []
|
|
for track in selectedTracksAndItems:
|
|
chordlists.append(list())
|
|
#[(object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), ...], #track 1
|
|
#[(object, {"keySignature": keysigobject}), (object, {"keySignature": keysigobject}), ...], #track 2
|
|
|
|
if len(track) >= 2:
|
|
for item, keysig in track:
|
|
if type(item) is items.Chord:
|
|
chordlists[-1].append(item)
|
|
|
|
if not tuple(flatList(chordlists)):
|
|
return None, None
|
|
return listOfChangedTrackIds, chordlists
|
|
else:
|
|
return None, None
|
|
|
|
|
|
def _reorderChords(functionToGenerateChordOrder, descriptionString):
|
|
"""Works track by track.
|
|
It is assumed that each chord is only once in generatorOfChords and also exists in the score
|
|
"""
|
|
listOfChangedTrackIds, chordlists = _listOChordsFromSelection()
|
|
if not listOfChangedTrackIds:
|
|
return
|
|
|
|
orderBeforeInsert = session.data.getBlockAndItemOrder()
|
|
moveFunction = _createLambdaMoveToForCurrentPosition()
|
|
def registeredUndoFunction():
|
|
moveFunction()
|
|
_changeBlockAndItemOrder(orderBeforeInsert)
|
|
session.history.register(registeredUndoFunction, descriptionString)
|
|
|
|
seentest = set()
|
|
|
|
recreateSelection = _createLambdaRecreateSelection()
|
|
|
|
for track in chordlists: #read-only
|
|
generatorOfChords = functionToGenerateChordOrder(track)
|
|
|
|
#We need to gather the indices of the original items all at once.
|
|
#Otherwise, if we replace step by step, we will find items multiple times leading to little or no shuffling.
|
|
|
|
originalOrder = [] #tuples. we delete from the original track so the indices will be all wrong in the end, except the first! and after we insert a new item at that first position the second index will be correct again. and so on...
|
|
for chord in track: #iterate over the original order
|
|
#Replace each chord with a random one by removing the original chord from block data. This keeps the chord objects unchanged, ready for undo by simply reordering.
|
|
assert not chord in seentest #shared between all tracks.
|
|
seentest.add(chord)
|
|
data = next(bl for |