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.

2991 lines
128 KiB

4 years ago
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
4 years ago
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
4 years ago
Laborejo2 is free software: you can redistribute it and/or modify
4 years ago
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")
4 years ago
#Python Standard Library
import sys
import random
random.seed()
from typing import Iterable, Callable, Tuple
4 years ago
#Template Modules
from template.calfbox import cbox
4 years ago
import template.engine.api #we need direct access to the module to inject data in the provided structures. but we also need the functions directly. next line:
from template.engine.api import *
from template.engine.duration import DM, DB, DL, D1, D2, D4, D8, D16, D32, D64, D128, D256, D512, D1024, D_DEFAULT, D_STACCATO, D_TENUTO, D_TIE
4 years ago
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
4 years ago
from .ccsubtrack import GraphItem, GraphTrackCC
4 years ago
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 = []
4 years ago
#self.playbackStart = []
#self.playbackStop = []
self._cachedTickIndex = -1
self.updateGraphTrackCC = []
self.graphCCTracksChanged = []
self.updateGraphBlockTrack = []
self.updateTempoTrack = []
self.updateTempoTrackBlocks = []
self.updateTempoTrackMeta = []
4 years ago
self.tempoScalingChanged = []
4 years ago
self.prevailingBaseDurationChanged = []
4 years ago
#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:
4 years ago
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:
4 years ago
setMetronome(track.asMetronomeData, label=track.name) #template api
4 years ago
def _updateTrack(self, trId):
4 years ago
"""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()
4 years ago
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)
4 years ago
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)
4 years ago
ex = gTr.staticGraphBlocksRepresentation()
for func in self.updateGraphBlockTrack:
func(trId, cc, ex)
4 years ago
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!"""
4 years ago
#TODO: moved?
if self.graphCCTracksChanged:
4 years ago
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.
4 years ago
This callback is relatively cheap because it does not generate
any item data."""
4 years ago
#TODO: does NOT call template.api._numberOfTracksChanged
4 years ago
session.data.updateJackMetadataSorting()
ex = session.data.listOfStaticTrackRepresentations()
if self.tracksChanged:
4 years ago
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()
4 years ago
session.data.metronome.label = session.data.currentMetronomeTrack.name #not the expensive redundant data generation
4 years ago
self._metronomeChanged() #because the track name is sent here.
def _updateTempoTrack(self):
"""Sends the block update as well.
staticRepresentations also updates midi.
4 years ago
Of course the order is: track meta, then blocks first, then items.
This must not change! The GUI depends on it."""
4 years ago
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)
4 years ago
def _tempoScalingChanged(self, newValue):
for func in self.tempoScalingChanged:
func(newValue)
4 years ago
def _playbackStart(self):
for func in self.playbackStart:
func()
def _playbackStop(self):
for func in self.playbackStop:
func()
def _prevailingBaseDurationChanged(self, newPrevailingBaseDuration):
for func in self.prevailingBaseDurationChanged:
func(newPrevailingBaseDuration)
def _historyChanged(self):
"""sends two lists of strings.
the first is the undoHistory, the last added item is [-1]. We can show that to a user to
indicate what the next undo will do.
the second is redoHistory, same as undo: [-1] shows the next redo action."""
undoHistory, redoHistory = session.history.asList()
for func in self.historyChanged:
func(undoHistory, redoHistory)
def _recordingStreamNoteOn(self, liveChord):
"""One dict at a time"""
trId = id(session.data.currentTrack())
for func in self.recordingStreamNoteOn:
func(trId, liveChord)
def _recordingStreamNoteOff(self, liveChord):
"""One dict at a time"""
trId = id(session.data.currentTrack())
for func in self.recordingStreamNoteOff:
func(trId, liveChord)
def _recordingStreamClear(self):
for func in self.recordingStreamClear:
func()
#Inject our derived Callbacks into the parent module
template.engine.api.callbacks = ClientCallbacks()
from template.engine.api import callbacks
_templateStartEngine = startEngine
def startEngine(nsmClient, additionalData:dict={}):
4 years ago
_templateStartEngine(nsmClient)
#Send initial Data etc.
session.data.tempoMap.isTransportMaster = True #always true for Laborejo.
4 years ago
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.
4 years ago
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.
4 years ago
for trId in session.data.listOfTrackIds():
callbacks._graphCCTracksChanged(trId) #create structure: all CC graphs, accessed by CC number (0-127)
4 years ago
for trId in session.data.listOfTrackIds():
callbacks._updateTrack(trId) #create content: music items
callbacks._updateTempoTrack() # does everything at once
4 years ago
#done above. Special situation at file load. callbacks._graphCCTracksChanged(trId) #create structure: all CC graphs, accessed by CC number (0-127)
4 years ago
for cc in session.data.trackById(trId).listOfCCsInThisTrack():
callbacks._updateGraphTrackCC(trId, cc) #create content: CC points. user points and interpolated points.
if session.data.currentMetronomeTrack.asMetronomeData:
setMetronome(session.data.currentMetronomeTrack.asMetronomeData, label=session.data.currentMetronomeTrack.name) #track.asMetronomeData is generated in staticRepresentation #template api. has callbacks
callbacks._setCursor()
if session.data.metaData["title"]:
cbox.JackIO.Metadata.client_set_property("http://jackaudio.org/metadata/pretty-name", session.data.metaData["title"])
4 years ago
global laborejoEngineStarted #makes for a convenient check. stepMidiInput uses it, which needs to know that the gui already started the api.
laborejoEngineStarted = True
4 years ago
#General and abstract Commands
def getMetadata():
"""Do not confuse with template/config METADATA. This is Lilypond title, composer etc."""
return session.data.metaData
def setMetadata(data):
titleBefore = bool(session.data.metaData["title"])
session.data.metaData = data
if session.data.metaData["title"]:
cbox.JackIO.Metadata.client_set_property("http://jackaudio.org/metadata/pretty-name", session.data.metaData["title"])
elif titleBefore:
cbox.JackIO.Metadata.client_remove_property("http://jackaudio.org/metadata/pretty-name")
4 years ago
def playFromCursor():
playFrom(ticks=session.data.cursorExport()["tickindex"])
4 years ago
def playFromBlockStart():
tr = session.data.currentTrack()
ticks = 0
for idx, bl in enumerate(tr.blocks):
if idx == tr.state.blockindex:
playFrom(ticks)
return
else:
ticks += bl.duration()
else:
raise RuntimeError("reached end of blocks without matchin current block index")
def getMidiInputNameAndUuid()->(str, int): #tuple name:str, uuid
"""Override template function. We access the stepMidi directly.
Used by the quick midi input widget
Return name and cboxMidiPortUid.
name is Client:Port JACK format
If not return None, None
"""
from engine.midiinput.stepmidiinput import stepMidiInput #singleton instance #avoid circular dependency. stepMidiInput import api
if stepMidiInput.ready: #startup delay
return stepMidiInput.fullName(), stepMidiInput.cboxMidiPortUid
else:
return None, None
4 years ago
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
4 years ago
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():
4 years ago
moveFunction()
_changeBlockAndItemOrder(orderBeforeInsert)
session.history.register(registeredUndoFunction, descriptionString=f"Insert {item}")
simpleCommand(nothing, autoStepLeft = False) #for callbacks
4 years ago
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.
4 years ago
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
4 years ago
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():
4 years ago
moveFunction()
_changeBlockAndItemOrder(orderBeforeInsert)
session.history.register(registeredUndoFunction, descriptionString="change order")
4 years ago
#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()
4 years ago
listOfChangedTrackIDs = session.data.deleteSelection()
if listOfChangedTrackIDs: #delete succeeded.
def registeredUndoFunction():
4 years ago
moveFunction()
_changeBlockAndItemOrder(orderBeforeDelete)
session.history.register(registeredUndoFunction, descriptionString="delete selection")
4 years ago
#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
4 years ago
"""api.duplicate overrides default paste behaviour by providing its own copyBuffer
and not destroying the selection/keep the cursor at its origin position
"""
4 years ago
dataBefore = session.data.getBlockAndItemOrder()
moveFunction = _createLambdaMoveToForCurrentPosition()
listOfChangedTrackIDs = session.data.pasteObjects(customBuffer, overwriteSelection)
if listOfChangedTrackIDs: #paste succeeded.
def registeredUndoFunction():
4 years ago
moveFunction()
_changeBlockAndItemOrder(dataBefore)
session.history.register(registeredUndoFunction, descriptionString="paste")
4 years ago
#Make changes visible in the GUI
for trackId in listOfChangedTrackIDs:
callbacks._updateTrack(trackId)
4 years ago
if updateCursor:
4 years ago
callbacks._setCursor()
def pasteObjectsTransposedReal(root:int=None, toPitch:int=None, adjustToKeySignature=False):
"""Uses the global/session clipboard buffer but pastes a transposed version, starting on the
pitch cursor position, that is adjusted to the current keysignature.
The notes are transformed into a custom buffer. We use standard paste for everything else.
If root or toPitch for the transposition interval are None they will derive their pitch from
the first pitch in the copy buffer and the cursor position, respectively.
"""
if not session.data.copyObjectsBuffer:
return #guard
copyBuffer = session.data.getIndenpendetCopyObjectsBuffer()
#Determine the first of the two pitches for our transposition interval
if root is None or not type(root) is int:
#First iteration only until the very first note, which we use to calculate the interval
for track in copyBuffer:
for item in track:
if type(item) is items.Chord:
root = item.notelist[0].pitch #ordered by ascending pitch
break
else: #inner loop finished without break. No chord? in the first track.
logging.warning("Found copy buffer without note in the first track. This is worth an investigation.")
continue #jump to the start and don't execute the outer loop break
break #outer loop. We only get here if the inner loop did break as well.
#Final Sanity Check. I don't think selections without chords are even allowed...
if root is None:
logging.error("pasteObjectsTransposedModal without notes in the copy buffer! (but not empty)")
return
if toPitch is None:
toPitch = getCursorPitch()
keysig = session.data.currentTrack().state.keySignature()
#Edit the copy buffer in place. We don't modify the list, just the contents.
for track in copyBuffer:
for item in track:
if type(item) is items.Chord:
item.intervalAutomatic(root, toPitch)
if adjustToKeySignature:
item.adjustToKeySignature([keysig,]) #keysig must be in a list because it is a chord. If it is just len==1 the transpose function will deal with it correctly.
pasteObjects(customBuffer=copyBuffer) #normal paste except our special buffer
def pasteObjectsTransposedModal(root:int=None, toPitch:int=None):
pasteObjectsTransposedReal(root, toPitch, adjustToKeySignature=True)
def duplicate(howOften:int): #ctrl+d
"""Duplicate a single object and put it right of the original. The cursor moves with it
4 years ago
to enable follow up insertion.
4 years ago
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."""
4 years ago
if session.data.cursorWhenSelectionStarted:
customBuffer = session.data.copyObjects(writeInSessionBuffer = False)
4 years ago
if customBuffer: #success
session.data.goToSelectionStart()
pos = session.data.where() #where even keeps the local position if a content linked block inserts items before our position
with session.history.sequence("duplicate selection"):
for i in range(howOften):
pasteObjects(customBuffer = customBuffer, updateCursor = False, overwriteSelection = False) #handles undo
session.data.goTo(*pos)
callbacks._setCursor(destroySelection=False)
else:
4 years ago
item = session.data.currentItem()
if item:
with session.history.sequence("duplicate item"):
for i in range(howOften):
insertItem(item.copy())
4 years ago
callbacks._setCursor()
4 years ago
#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()
4 years ago
_updateCallbackAllTracks()
4 years ago
def useCurrentTrackAsMetronome():
"""This is called once after loading/creating a session in startEngine"""
session.data.currentMetronomeTrack = session.data.currentTrack()
4 years ago
setMetronome(session.data.currentMetronomeTrack.asMetronomeData, label=session.data.currentMetronomeTrack.name) #template api. has callbacks
4 years ago
#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():
4 years ago
moveFunction()
deleteTrack(newTrackId)
session.history.register(registeredUndoFunction, descriptionString = "insert track")
session.data.calculateAudibleSoloForCbox()
4 years ago
callbacks._tracksChanged()
callbacks._updateTrack(newTrackId)
callbacks._setCursor()
#search tags: newTrack addTrack
4 years ago
def newEmptyTrack():
"""Append an empty track and switch to the new track"""
newIndex = len(session.data.tracks)
newTrack = Track(session.data)
if newIndex > 0:
newTrack.initialKeySignature = session.data.tracks[0].initialKeySignature.copy()
newTrack.initialMetricalInstruction = session.data.tracks[0].initialMetricalInstruction.copy()
newTrack.upbeatInTicks = session.data.tracks[0].upbeatInTicks #just an int
4 years ago
insertTrack(newIndex, newTrack) #handles callbacks and undo
return (id(newTrack))
4 years ago
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():
4 years ago
trackObject.sequencerInterface.recreateThroughUndo()
insertTrack(trackIndex, trackObject)
4 years ago
session.history.register(registeredUndoFunction, descriptionString = "delete track")
callbacks._tracksChanged()
callbacks._setCursor()
if trackObject is session.data.currentMetronomeTrack:
useCurrentTrackAsMetronome() #we already have a new current one
session.data.calculateAudibleSoloForCbox()
4 years ago
def deleteCurrentTrack():
deleteTrack(id(session.data.currentTrack()))
def hideTrack(trId):
"""For the callbacks this looks like a delete. But there is no undo.
The track still emits playback.
hide and unhide track register in the history. deleteItem depends on the item to be in a visible
track so it may possible to insert an item, hide the track and then undo which would try to
delete an item in a hidden track. Therefore hide and unhide register.
"""
trackObject = session.data.trackById(trId)
result = session.data.hideTrack(trackObject)
if result: #not the only track
session.history.register(lambda trId=trId: unhideTrack(trId), descriptionString = "hide track")
callbacks._tracksChanged() #even if there is no change the GUI needs to be notified to redraw its checkboxes that may have been enabled GUI-side.
callbacks._setCursor()
def unhideTrack(trId):
trackObject = session.data.trackById(trId)
session.data.unhideTrack(trackObject) #always succeeds, or throws error.
session.history.register(lambda trId=trId: hideTrack(trId), descriptionString = "unhide track")
callbacks._tracksChanged()
callbacks._updateTrack(trId)
#the cursor is uneffected
def trackAudible(trId, state:bool):
"""
Aka. mute, but we don't call it like this because:
Send midi notes or not. CCs and instrument changes are unaffected.
Not managed by undo/redo.
Does not need updateTrack. There is no new midi data to generate. cbox handles mute on its own
Audible will shut off any output, no matter if solo or not.
Solo will determine which of the audible tracks are played.
Like in any DAW. Inverted Solo logic etc.
"""
session.data.trackById(trId).audible = state
session.data.calculateAudibleSoloForCbox()
callbacks._tracksChanged() #even if there is no change the GUI needs to be notified to redraw its checkboxes that may have been enabled GUI-side.
def trackSolo(trId, state:bool):
"""
Another layer like audible tracks.
This is the classic solo/mute duality.
Audible will shut off any output, no matter if solo or not.
Solo will determine which of the audible tracks are played.
Like in any DAW. Inverted Solo logic etc.
Not managed by undo/redo.
Does not need updateTrack. There is no new midi data to generate. cbox handles mute on its own
"""
session.data.trackById(trId).solo = state
session.data.calculateAudibleSoloForCbox()
callbacks._setCursor() #the cursor includes solo export for the current track.
callbacks._tracksChanged() #even if there is no change the GUI needs to be notified to redraw its checkboxes that may have been enabled GUI-side.
def toggleCurrentTrackSolo():
"""trackSolo, but for the cursor. And toggle"""
track = session.data.currentTrack()
trackSolo(id(track), not track.solo)
def resetAllSolo():
for track in session.data.tracks + list(session.data.hiddenTracks.keys()):
track.solo = False
session.data.calculateAudibleSoloForCbox()
callbacks._setCursor() #the cursor includes solo export for the current track.
callbacks._tracksChanged() #even if there is no change the GUI needs to be notified to redraw its checkboxes that may have been enabled GUI-side.
4 years ago
def listOfTrackIds():
return session.data.listOfTrackIds()
def listOfHiddenTrackIds():
return [id(track) for track in session.data.hiddenTracks.keys()]
def rearrangeTracks(listOfTrackIds):
if len(session.data.tracks) <= 1:
return
session.history.register(lambda l=session.data.asListOfTrackIds(): rearrangeTracks(l), descriptionString = "rearrange tracks")
session.data.rearrangeTracks(listOfTrackIds)
callbacks._tracksChanged()
callbacks._setCursor()
def setTrackName(trId, nameString, initialInstrumentName, initialShortInstrumentName):
trackObject = session.data.trackById(trId)
session.history.register(lambda i=trId, n=trackObject.name, lyN=trackObject.initialInstrumentName, lySN=trackObject.initialShortInstrumentName: setTrackName(i, n, lyN, lySN), descriptionString = "change track name")
trackObject.name = nameString # this is a setter. It changes calfbox as well.
trackObject.initialInstrumentName = initialInstrumentName
trackObject.initialShortInstrumentName = initialShortInstrumentName
callbacks._tracksChanged()
callbacks._setCursor() #cursor contains track name and thus needs updating
4 years ago
def setTrackUpbeat(trId, upbeatInTicks):
trackObject = session.data.trackById(trId)
session.history.register(lambda i=trId, u=trackObject.upbeatInTicks: setTrackUpbeat(i, u), descriptionString = "change track upbeat")
trackObject.upbeatInTicks = upbeatInTicks
callbacks._updateTrack(trId)
def setDoubleTrack(trId, statusBool):
"""It does not touch any important data because it is more or less
a savefile-persistent visual convenience feature.
So we don't need undo/redo"""
trackObject = session.data.trackById(trId)
trackObject.double = statusBool
callbacks._tracksChanged()
callbacks._updateTrack(trId)
callbacks._setCursor()
def setTrackSettings(trId, dictionary):
"""We need to create a new playback of the track to update the midi data."""
trackObject = session.data.trackById(trId)
previousSettings = trackObject.staticTrackRepresentation()
clean = True
for key, value in dictionary.items():
#this assumes keys are the same as track export. Will give a key error at least if not.
if not previousSettings[key] == value:
clean = False
break
if not clean:
trackObject.initialClefKeyword = dictionary["initialClefKeyword"]
4 years ago
trackObject.initialMidiChannel = dictionary["initialMidiChannel"]
trackObject.initialMidiBankMsb = dictionary["initialMidiBankMsb"]
trackObject.initialMidiBankLsb = dictionary["initialMidiBankLsb"]
trackObject.initialMidiProgram = dictionary["initialMidiProgram"]
trackObject.ccChannels = dictionary["ccChannels"]
trackObject.midiTranspose = dictionary["midiTranspose"]
4 years ago
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"]
4 years ago
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
4 years ago
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)
4 years ago
callbacks._updateTrack(trId)
def resetDuationSettingsSignature(trId):
trackObject = session.data.trackById(trId)
previousSettings = trackObject.staticTrackRepresentation()
trackObject.durationSettingsSignature.reset()
session.history.register(lambda trId=trId, previousSettings=previousSettings: setTrackSettings(trId, previousSettings), descriptionString = "reset track duration settings")
callbacks._tracksChanged()
#We only need this for midi changes. callbacks._updateSingleTrackAllCC(trId)
callbacks._updateTrack(trId)
#Blocks
def currentBlockExport():
"""Return the static export item of the current block.
Compatible with getDataAsDict and putDataFromDict
such as names and minimum tick duration"""
return session.data.currentTrack().currentBlock().getDataAsDict()
4 years ago
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():
4 years ago
moveFunction()
deleteBlock(id(block))
session.history.register(registeredUndoFunction, descriptionString = "append block")
session.data.currentTrack().right()
else:
session.history.register(lambda blId = id(block): deleteBlock(blId), descriptionString = "append block")
callbacks._updateTrack(id(tr))
#callbacks._setCursor() #this will make the GUI jump around because it centers on the cursor
4 years ago
def splitBlock():
tr = session.data.currentTrack()
if tr.state.isAppending() and len(tr.blocks)-1 == tr.state.blockindex: #end of track? Yes len-1 here and len-2 in append() is correct. I don't know why :(
appendBlock()
else: #a real split
dictOfTrackIdsWithListOfBlockIds = session.data.splitCurrentBlockAndAllContentLinks() #do the split. We get the current state as return value for undo
if dictOfTrackIdsWithListOfBlockIds:
rearrangeBlocksInMultipleTracks(dictOfTrackIdsWithListOfBlockIds) #handles undo and callbacks for redrawing
callbacks._setCursor() #cursor is still on the same item. But the item might be further to the right now when additional block boundaries have been introduced to the left through contentLinks
right() # continue typing on the right side. This was added in 2020 to create a "split is an item" feeling
4 years ago
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