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.

765 lines
29 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import logging; logger = logging.getLogger(__name__); logger.info("import")
6 years ago
#Standard Library Modules
from typing import List, Set, Dict, Tuple
#Third Party Modules
from calfbox import cbox
#Template Modules
import template.engine.api #we need direct access to the module to inject data in the provided structures. but we also need the functions directly. next line:
from template.engine.api import *
from template.engine.duration import baseDurationToTraditionalNumber
6 years ago
#Our modules
from .pattern import NUMBER_OF_STEPS
DEFAULT_FACTOR = 1 #for the GUI.
#New callbacks
class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
def __init__(self):
super().__init__()
6 years ago
self.timeSignatureChanged = []
self.scoreChanged = []
self.numberOfMeasuresChanged = []
6 years ago
self.trackStructureChanged = []
6 years ago
self.trackMetaDataChanged = []
self.patternChanged = []
self.stepChanged = []
self.removeStep = []
self.exportCacheChanged = []
self.subdivisionsChanged = []
self.quarterNotesPerMinuteChanged = []
self.loopChanged = []
def _quarterNotesPerMinuteChanged(self):
"""There is one tempo for the entire song in quarter notes per mintue.
score.isTransportMaster to False means we do not create our own changes
and leave everything to the default. Negative values are not possible"""
6 years ago
if session.data.tempoMap.isTransportMaster:
export = session.data.tempoMap.getQuarterNotesPerMinute()
6 years ago
else:
export = None
for func in self.quarterNotesPerMinuteChanged:
func(export)
callbacks._dataChanged()
6 years ago
6 years ago
def _setPlaybackTicks(self): #Differs from the template because it has subdivisions.
ppqn = cbox.Transport.status().pos_ppqn * session.data.subdivisions
6 years ago
status = playbackStatus()
6 years ago
for func in self.setPlaybackTicks:
func(ppqn, status)
6 years ago
def _loopChanged(self, measurenumber, loopStart, loopEnd):
export = measurenumber
for func in self.loopChanged:
func(export)
6 years ago
def _timeSignatureChanged(self):
nr = session.data.howManyUnits
typ = session.data.whatTypeOfUnit
6 years ago
for func in self.timeSignatureChanged:
func(nr, typ)
##All patterns and tracks need updates:
for track in session.data.tracks:
6 years ago
self._patternChanged(track)
self._subdivisionsChanged() #update subdivisions. We do not include them in the score or track export on purpose.
callbacks._dataChanged()
6 years ago
def _subdivisionsChanged(self):
"""Subdivisions are tricky, therefore we keep them isolated in their own callback.
You don't need to redraw anything if you don't want to. One recommendation is to
draw every n step a little more important (bigger, different color).
where n = subdivions
We also place JACK BBT via tempoMap here because we need it every time the time sig
changes (which calls _subdivisionsChanged) and if subdivisions change.
"""
typ = baseDurationToTraditionalNumber[session.data.whatTypeOfUnit]
nr = session.data.howManyUnits
tradNr = int(nr / session.data.subdivisions)
#JACK BBT for Timebase Master. No matter if we are master at the moment or not.
if tradNr == nr / session.data.subdivisions:
#Easier to read than the else case. Not possible with 9 Steps per Pattern in Groups of 2 because that is a 4.5/4 timesig.
session.data.tempoMap.setTimeSignature(tradNr, typ)
else:
#Always possible, compared to first if case.
session.data.tempoMap.setTimeSignature(nr, typ*session.data.subdivisions)
export = session.data.subdivisions
6 years ago
for func in self.subdivisionsChanged:
func(export)
callbacks._dataChanged()
6 years ago
def _scoreChanged(self):
"""This includes the time signature as well, but is not send on a timesig change.
A timesig change needs to update all tracks as playback as well as for the GUI
so it is its own callback.
Use this for fast and inexpensive updates like drawing a label or adjusting the
GUI that shows your measure groups (a label each 8 measures or so)"""
export = session.data.export()
6 years ago
for func in self.scoreChanged:
func(export)
callbacks._dataChanged()
6 years ago
def _exportCacheChanged(self, track):
"""Send the export cache for GUI caching reasons. Don't react by redrawing immediately!
This is sent often, redundantly and more than you need.
Example: If you only show one pattern at the same time use this to cache the data
in all hidden patterns and redraw only if you change the active pattern.
You can react to real changes in your active pattern by using patternChanged
and stepChanged."""
export = track.export()
for func in self.exportCacheChanged:
func(export)
6 years ago
def _patternChanged(self, track):
"""each track has only one pattern. We can identify the pattern by track and vice versa.
Don't use this to react to clicks on the pattern editor. Use stepChanged instead and
keep book of your incremental updates.
This is used for the whole pattern: timesig changes, invert, clean etc.
"""
export = track.export()
self._exportCacheChanged(track)
for func in self.patternChanged:
func(export)
callbacks._dataChanged()
6 years ago
def _stepChanged(self, track, stepDict):
"""A simple GUI will most like not listen to that callback since they
already changed the step on their side. Only useful for parallel
views.
We do not export anything but just sent back the change we received
as dict message."""
self._exportCacheChanged(track)
for func in self.stepChanged:
func(stepDict)
callbacks._dataChanged()
6 years ago
def _removeStep(self, track, index, pitch):
"""Opposite of _stepChanged"""
self._exportCacheChanged(track)
for func in self.stepChanged:
func(index, pitch)
callbacks._dataChanged()
6 years ago
6 years ago
def _trackStructureChanged(self, track):
"""update one track structure. Does not export cbox.
Also includes transposition """
export = track.export()
for func in self.trackStructureChanged:
func(export)
callbacks._dataChanged()
6 years ago
6 years ago
def _trackMetaDataChanged(self, track):
"""a low cost function that should not trigger anything costly to redraw
but some text and simple widgets."""
export = track.export()
6 years ago
for func in self.trackMetaDataChanged:
func(export)
callbacks._dataChanged()
6 years ago
def _numberOfMeasuresChanged(self):
export = session.data.export()
6 years ago
for func in self.numberOfMeasuresChanged:
func(export)
callbacks._dataChanged()
6 years ago
#Inject our derived Callbacks into the parent module
template.engine.api.callbacks = ClientCallbacks()
from template.engine.api import callbacks
_templateStartEngine = startEngine
6 years ago
def updatePlayback():
#TODO: use template.sequencer.py internal updates instead
cbox.Document.get_song().update_playback()
def startEngine(nsmClient):
_templateStartEngine(nsmClient)
6 years ago
session.inLoopMode = None # remember if we are in loop mode or not. Positive value is None or a tuple with start and end
#Send initial Callbacks to create the first GUI state.
6 years ago
#The order of initial callbacks must not change to avoid GUI problems.
#For example it is important that the tracks get created first and only then the number of measures
logger.info("Sending initial callbacks to GUI")
callbacks._numberOfTracksChanged()
callbacks._timeSignatureChanged()
callbacks._numberOfMeasuresChanged()
callbacks._subdivisionsChanged()
callbacks._quarterNotesPerMinuteChanged()
6 years ago
for track in session.data.tracks:
6 years ago
callbacks._trackMetaDataChanged(track) #for colors, scale and simpleNoteNames
6 years ago
session.data.buildAllTracks(buildSongDuration=True) #will set to max track length, we always have a song duration.
updatePlayback()
logger.info("Patroneo api startEngine complete")
def _loopOff():
session.data.buildSongDuration() #no parameter removes the loop
updatePlayback()
session.inLoopMode = None
callbacks._loopChanged(None, None, None)
def _loopNow():
now = cbox.Transport.status().pos_ppqn
_setLoop(now)
def _setLoop(loopMeasureAroundPpqn):
if loopMeasureAroundPpqn < 0:
_loopOff()
return
loopStart, loopEnd = session.data.buildSongDuration(loopMeasureAroundPpqn)
6 years ago
updatePlayback()
session.inLoopMode = (loopStart, loopEnd)
assert loopStart <= loopMeasureAroundPpqn < loopEnd
if not playbackStatus():
cbox.Transport.play()
oneMeasureInTicks = (session.data.howManyUnits * session.data.whatTypeOfUnit) / session.data.subdivisions
measurenumber, rest = divmod(loopStart, oneMeasureInTicks)
callbacks._loopChanged(int(measurenumber), loopStart, loopEnd)
6 years ago
def toggleLoop():
"""Plays the current measure as loop.
6 years ago
Current measure is where the playback cursor is
session.inLoopMode is a tuple (start, end)
"""
6 years ago
if session.inLoopMode:
_loopOff()
6 years ago
else:
_loopNow()
6 years ago
def rewind():
"""template.toStart, but removes our loop"""
_loopOff()
toStart()
6 years ago
def seek(value):
"""override template one, which does not have a loop"""
if value < 0:
value = 0
if session.inLoopMode and not session.inLoopMode[0] <= value < session.inLoopMode[1]: #if you seek outside the loop the loop will be destroyed.
toggleLoop()
cbox.Transport.seek_ppqn(value)
##Score
def set_quarterNotesPerMinute(value):
6 years ago
if value is None:
6 years ago
session.data.tempoMap.isTransportMaster = False #triggers rebuild
6 years ago
elif value == "on":
6 years ago
assert not session.data.tempoMap.isTransportMaster
#keep old bpm value. 120 bpm is default.
session.data.tempoMap.isTransportMaster = True #triggers rebuild
6 years ago
else:
assert value > 0
6 years ago
session.data.tempoMap.setQuarterNotesPerMinute(value)
session.data.tempoMap.isTransportMaster = True #triggers rebuild
6 years ago
#Does not need track rebuilding
6 years ago
updatePlayback()
callbacks._quarterNotesPerMinuteChanged()
6 years ago
def set_whatTypeOfUnit(ticks):
"""Denominator of Time Signature"""
if session.data.whatTypeOfUnit == ticks: return
session.data.whatTypeOfUnit = ticks
session.data.buildAllTracks()
if session.inLoopMode:
_loopNow()
6 years ago
updatePlayback()
callbacks._timeSignatureChanged()
6 years ago
def set_howManyUnits(value):
"""Numerator of Time Signature"""
if session.data.howManyUnits == value: return
session.data.howManyUnits = value
session.data.buildAllTracks()
if session.inLoopMode:
_loopNow()
6 years ago
updatePlayback()
callbacks._timeSignatureChanged()
6 years ago
def set_subdivisions(value):
if session.data.subdivisions == value: return
session.data.subdivisions = value
session.data.buildAllTracks()
if session.inLoopMode:
_loopNow()
6 years ago
updatePlayback()
callbacks._subdivisionsChanged()
6 years ago
def convert_subdivisions(value, errorHandling):
""""errorHandling can be fail, delete or merge"""
if session.data.subdivisions == value: return
result = session.data.convertSubdivisions(value, errorHandling)
6 years ago
if result:
session.data.buildAllTracks()
6 years ago
updatePlayback()
callbacks._timeSignatureChanged() #includes subdivisions
for tr in session.data.tracks:
callbacks._patternChanged(tr)
6 years ago
else:
callbacks._subdivisionsChanged() #to reset the GUI value back to the working one.
if session.inLoopMode:
_loopNow()
6 years ago
return result
def set_numberOfMeasures(value):
if session.data.numberOfMeasures == value: return
session.data.numberOfMeasures = value
session.data.buildSongDuration()
6 years ago
updatePlayback()
callbacks._numberOfMeasuresChanged()
callbacks._scoreChanged() #redundant but cheap and convenient
6 years ago
def set_measuresPerGroup(value):
if session.data.measuresPerGroup == value: return
session.data.measuresPerGroup = value
6 years ago
#No playback change
callbacks._scoreChanged()
6 years ago
def changeTrackName(trackId, name):
track = session.data.trackById(trackId)
6 years ago
track.sequencerInterface.name = " ".join(name.split())
callbacks._trackMetaDataChanged(track)
6 years ago
def changeTrackColor(trackId, colorInHex):
"""Expects "#rrggbb"""
track = session.data.trackById(trackId)
6 years ago
assert len(colorInHex) == 7, colorInHex
track.color = colorInHex
callbacks._trackMetaDataChanged(track)
6 years ago
def addTrack(scale=None):
if scale:
assert type(scale) == tuple
session.data.addTrack(scale=scale)
callbacks._numberOfTracksChanged()
6 years ago
def createSiblingTrack(trackId):
"""Create a new track with scale, color and jack midi out the same as the given track.
The jack midi out will be independent after creation, but connected to the same instrument
(if any)"""
track = session.data.trackById(trackId)
6 years ago
assert type(track.pattern.scale) == tuple
6 years ago
newTrack = session.data.addTrack(name=track.sequencerInterface.name, scale=track.pattern.scale, color=track.color, simpleNoteNames=track.pattern.simpleNoteNames) #track name increments itself from "Track A" to "Track B" or "Track 1" -> "Track 2"
6 years ago
newTrack.pattern.averageVelocity = track.pattern.averageVelocity
6 years ago
jackConnections = cbox.JackIO.get_connected_ports(track.sequencerInterface.cboxPortName())
6 years ago
for port in jackConnections:
6 years ago
cbox.JackIO.port_connect(newTrack.sequencerInterface.cboxPortName(), port)
6 years ago
#Move new track to neighbour the old one.
oldIndex = session.data.tracks.index(track)
newIndex = session.data.tracks.index(newTrack)
newTrackAgain = session.data.tracks.pop(newIndex)
6 years ago
assert newTrackAgain is newTrack
session.data.tracks.insert(oldIndex+1, newTrackAgain)
callbacks._numberOfTracksChanged()
6 years ago
return newTrack.export()
def deleteTrack(trackId):
track = session.data.trackById(trackId)
session.data.deleteTrack(track)
if not session.data.tracks: #always keep at least one track
session.data.addTrack()
6 years ago
updatePlayback()
callbacks._numberOfTracksChanged()
6 years ago
def moveTrack(trackId, newIndex):
"""index is 0 based"""
track = session.data.trackById(trackId)
oldIndex = session.data.tracks.index(track)
6 years ago
if not oldIndex == newIndex:
session.data.tracks.pop(oldIndex)
session.data.tracks.insert(newIndex, track)
callbacks._numberOfTracksChanged()
6 years ago
#Track Switches
def setSwitches(trackId, setOfPositions, newBool):
track = session.data.trackById(trackId)
6 years ago
if newBool:
track.structure = track.structure.union(setOfPositions) #add setOfPositions to the existing one
else:
track.structure = track.structure.difference(setOfPositions) #remove everything from setOfPositions that is in the existing one, ignore entries from setOfPositions not in existing.
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackStructureChanged(track)
6 years ago
def setSwitch(trackId, position, newBool):
track = session.data.trackById(trackId)
6 years ago
if newBool:
if position in track.structure: return
track.structure.add(position)
else:
if not position in track.structure: return
track.structure.remove(position)
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackStructureChanged(track)
6 years ago
return True
def trackInvertSwitches(trackId):
track = session.data.trackById(trackId)
6 years ago
"""
if track.structure:
new = set(i for i in range(max(track.structure)))
track.structure = new.difference(track.structure)
else:
track.structure = set(i for i in range(session.data.numberOfMeasures))
6 years ago
"""
new = set(i for i in range(session.data.numberOfMeasures))
6 years ago
track.structure = new.difference(track.structure)
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackStructureChanged(track)
6 years ago
def trackOffAllSwitches(trackId):
track = session.data.trackById(trackId)
6 years ago
track.structure = set()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackStructureChanged(track)
6 years ago
def trackOnAllSwitches(trackId):
track = session.data.trackById(trackId)
track.structure = set(i for i in range(session.data.numberOfMeasures))
6 years ago
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackStructureChanged(track)
6 years ago
def trackMergeCopyFrom(sourceTrackId, targetTrackId):
if not sourceTrackId == targetTrackId:
sourceTrack = session.data.trackById(sourceTrackId)
targetTrack = session.data.trackById(targetTrackId)
6 years ago
targetTrack.structure = targetTrack.structure.union(sourceTrack.structure)
targetTrack.whichPatternsAreScaleTransposed.update(sourceTrack.whichPatternsAreScaleTransposed)
targetTrack.whichPatternsAreHalftoneTransposed.update(sourceTrack.whichPatternsAreHalftoneTransposed)
targetTrack.buildTrack()
6 years ago
updatePlayback()
callbacks._trackStructureChanged(targetTrack)
def trackPatternReplaceFrom(sourceTrackId, targetTrackId):
if not sourceTrackId == targetTrackId:
sourceTrack = session.data.trackById(sourceTrackId)
targetTrack = session.data.trackById(targetTrackId)
copyPattern = sourceTrack.pattern.copy(newParentTrack = targetTrack)
targetTrack.pattern = copyPattern
targetTrack.buildTrack()
updatePlayback()
callbacks._patternChanged(targetTrack)
6 years ago
def setSwitchScaleTranspose(trackId, position, transpose):
"""Scale transposition is flipped. lower value means higher pitch"""
track = session.data.trackById(trackId)
6 years ago
track.whichPatternsAreScaleTransposed[position] = transpose
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackStructureChanged(track)
6 years ago
return True
def setSwitchHalftoneTranspose(trackId, position, transpose):
"""Halftone transposition is not flipped. Higher value means higher pitch"""
track = session.data.trackById(trackId)
6 years ago
track.whichPatternsAreHalftoneTransposed[position] = transpose
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackStructureChanged(track)
6 years ago
return True
def insertSilence(howMany, beforeMeasureNumber):
"""Insert empty measures into all tracks"""
for track in session.data.tracks:
6 years ago
track.structure = set( (switch + howMany if switch >= beforeMeasureNumber else switch) for switch in track.structure )
track.whichPatternsAreScaleTransposed = { (k+howMany if k >= beforeMeasureNumber else k):v for k,v in track.whichPatternsAreScaleTransposed.items() }
track.whichPatternsAreHalftoneTransposed = { (k+howMany if k >= beforeMeasureNumber else k):v for k,v in track.whichPatternsAreHalftoneTransposed.items() }
callbacks._trackStructureChanged(track)
session.data.buildAllTracks()
6 years ago
updatePlayback()
6 years ago
def duplicateSwitchGroup(startMeasureForGroup:int, endMeasureExclusive:int):
groupSize = endMeasureExclusive-startMeasureForGroup
insertSilence(groupSize, endMeasureExclusive)
for track in session.data.tracks:
for switch in range(startMeasureForGroup+groupSize, endMeasureExclusive+groupSize): #One group after the given one.
if switch-groupSize in track.structure:
track.structure.add(switch)
if switch-groupSize in track.whichPatternsAreScaleTransposed:
track.whichPatternsAreScaleTransposed[switch] = track.whichPatternsAreScaleTransposed[switch-groupSize]
if switch-groupSize in track.whichPatternsAreHalftoneTransposed:
track.whichPatternsAreHalftoneTransposed[switch] = track.whichPatternsAreHalftoneTransposed[switch-groupSize]
callbacks._trackStructureChanged(track)
session.data.buildAllTracks()
updatePlayback()
def clearSwitchGroupTranspositions(startMeasureForGroup:int, endMeasureExclusive:int):
for track in session.data.tracks:
for switch in range(startMeasureForGroup, endMeasureExclusive):
if switch in track.whichPatternsAreScaleTransposed:
del track.whichPatternsAreScaleTransposed[switch]
if switch in track.whichPatternsAreHalftoneTransposed:
del track.whichPatternsAreHalftoneTransposed[switch]
callbacks._trackStructureChanged(track)
session.data.buildAllTracks()
updatePlayback()
6 years ago
def deleteSwitches(howMany, fromMeasureNumber):
for track in session.data.tracks:
6 years ago
new_structure = set()
for switch in track.structure:
if switch < fromMeasureNumber:
new_structure.add(switch)
elif switch >= fromMeasureNumber+howMany: #like a text editor let gravitate left into the hole left by the deleted range
new_structure.add(switch-howMany)
#else: #discard all in range to delete
track.structure = new_structure
new_scaleTransposed = dict()
for k,v in track.whichPatternsAreScaleTransposed.items():
if k < fromMeasureNumber:
new_scaleTransposed[k] = v
elif k >= fromMeasureNumber+howMany: #like a text editor let gravitate left into the hole left by the deleted range
new_scaleTransposed[k-howMany] = v
#else: #discard all in range to delete
track.whichPatternsAreScaleTransposed = new_scaleTransposed
new_halftoneTransposed = dict()
for k,v in track.whichPatternsAreHalftoneTransposed.items():
if k < fromMeasureNumber:
new_halftoneTransposed[k] = v
elif k >= fromMeasureNumber+howMany: #like a text editor let gravitate left into the hole left by the deleted range
new_halftoneTransposed[k-howMany] = v
#else: #discard all in range to delete
track.whichPatternsAreHalftoneTransposed = new_halftoneTransposed
callbacks._trackStructureChanged(track)
session.data.buildAllTracks()
6 years ago
updatePlayback()
6 years ago
#Pattern Steps
def setPattern(trackId, patternList):
"""Change the whole pattern, send a callback with the whole pattern"""
track = session.data.trackById(trackId)
6 years ago
track.pattern.data = patternList
track.pattern.buildExportCache()
track.buildTrack()
callbacks._patternChanged(track)
6 years ago
def getAverageVelocity(trackId):
"""If a GUI wants to add a new note and choose a sensible velocity it can use this function"""
return session.data.trackById(trackId).pattern.averageVelocity
6 years ago
def setStep(trackId, stepExportDict):
"""This is an atomic operation that only sets one switch and
only sends that switch back via callback. A simple GUI
will most like not listen to that callback since they
already changed the step on their side. Only useful for parallel
views."""
track = session.data.trackById(trackId)
6 years ago
oldNote = track.pattern.stepByIndexAndPitch(index=stepExportDict["index"], pitch=stepExportDict["pitch"])
if oldNote: #modify existing note
oldNoteIndex = track.pattern.data.index(oldNote)
track.pattern.data.remove(oldNote)
track.pattern.data.insert(oldNoteIndex, stepExportDict) #for what its worth, insert at the old place. It doesn't really matter though.
else: #new note
track.pattern.data.append(stepExportDict)
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._stepChanged(track, stepExportDict)
6 years ago
def removeStep(trackId, index, pitch):
"""Reverse of setStep"""
track = session.data.trackById(trackId)
6 years ago
oldNote = track.pattern.stepByIndexAndPitch(index, pitch)
track.pattern.data.remove(oldNote)
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._removeStep(track, index, pitch)
6 years ago
def setScale(trackId, scale, callback = True):
6 years ago
"""Expects a scale list or tuple from lowest index to highest.
Actual pitches don't matter."""
track = session.data.trackById(trackId)
6 years ago
track.pattern.scale = scale
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
if callback:
callbacks._trackMetaDataChanged(track)
6 years ago
6 years ago
def setSimpleNoteNames(trackId, simpleNoteNames):
6 years ago
"""note names is a list of strings with length 128. One name for each midi note.
It is saved to file"""
track = session.data.trackById(trackId)
6 years ago
track.pattern.simpleNoteNames = simpleNoteNames
callbacks._trackMetaDataChanged(track)
6 years ago
def transposeHalftoneSteps(trackId, steps):
track = session.data.trackById(trackId)
6 years ago
track.pattern.scale = [midipitch+steps for midipitch in track.pattern.scale]
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackMetaDataChanged(track)
6 years ago
def patternInvertSteps(trackId):
track = session.data.trackById(trackId)
6 years ago
track.pattern.invert()
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._patternChanged(track)
6 years ago
def patternOnAllSteps(trackId):
track = session.data.trackById(trackId)
6 years ago
track.pattern.fill()
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._patternChanged(track)
6 years ago
def patternOffAllSteps(trackId):
track = session.data.trackById(trackId)
6 years ago
track.pattern.empty()
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._patternChanged(track)
def patternInvertRow(trackId, pitchindex):
"""Pitchindex is the row"""
track = session.data.trackById(trackId)
track.pattern.invertRow(pitchindex)
track.pattern.buildExportCache()
track.buildTrack()
updatePlayback()
callbacks._patternChanged(track)
6 years ago
def patternClearRow(trackId, pitchindex):
"""Pitchindex is the row.
Index is the column"""
track = session.data.trackById(trackId)
track.pattern.clearRow(pitchindex)
track.pattern.buildExportCache()
track.buildTrack()
updatePlayback()
callbacks._patternChanged(track)
def patternRowRepeatFromStep(trackId, pitchindex, index):
"""Pitchindex is the row.
Index is the column"""
track = session.data.trackById(trackId)
track.pattern.repeatFromStep(pitchindex, index)
track.pattern.buildExportCache()
track.buildTrack()
updatePlayback()
callbacks._patternChanged(track)
6 years ago
def patternRowChangeVelocity(trackId, pitchindex, delta):
track = session.data.trackById(trackId)
for note in track.pattern.getRow(pitchindex):
new = note["velocity"] + delta
note["velocity"] = min(max(new,0), 127)
track.pattern.buildExportCache()
track.buildTrack()
updatePlayback()
callbacks._patternChanged(track)
6 years ago
major = [0, 2, 4, 5, 7, 9, 11, 12] #this if sorted by pitch, lowest to highest. Patroneo works in reverse order to accomodate the row/column approach of a grid. We reverse in setScaleToKeyword
schemesDict = {
#this if sorted by pitch, lowest to highest. Patroneo works in reverse order to accomodate the row/column approach of a grid. We reverse in setScaleToKeyword
"Major": [0,0,0,0,0,0,0,0],
"Minor": [0,0,-1,0,0,-1,-1,0],
"Dorian": [0,0,-1,0,0,0,-1,0],
"Phrygian": [0,-1,-1,0,0,-1,-1,0],
"Lydian": [0,0,0,+1,0,0,0,0],
"Mixolydian": [0,0,0,0,0,0,-1,0],
"Locrian": [0,-1,-1,0,-1,-1,-1,0],
"Blues": [0,-2,-1,0,-1,-2,-1,0],
"Hollywood": [0,0,0,0,0,-1,-1,0], #The "Hollywood"-Scale. Stargate, Lord of the Rings etc.
"Chromatic": [0,-1,-2,-2,-3,-4,-5,-5], #not a complete octave, but that is how it goes.
}
major.reverse()
for l in schemesDict.values():
l.reverse()
schemes = [
"Major",
"Minor",
"Dorian",
"Phrygian",
"Lydian",
"Mixolydian",
"Locrian",
"Blues",
"Hollywood",
"Chromatic",
]
def setScaleToKeyword(trackId, keyword):
track = session.data.trackById(trackId)
6 years ago
rememberRootNote = track.pattern.scale[-1] #no matter if this is the lowest or not%
scale = [x + y for x, y in zip(major, schemesDict[keyword])]
difference = rememberRootNote - scale[-1]
result = [midipitch+difference for midipitch in scale]
track.pattern.scale = result
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._trackMetaDataChanged(track)
6 years ago
def changePatternVelocity(trackId, steps):
track = session.data.trackById(trackId)
6 years ago
for note in track.pattern.data:
new = note["velocity"] + steps
note["velocity"] = min(max(new,0), 127)
track.pattern.buildExportCache()
track.buildTrack()
6 years ago
updatePlayback()
callbacks._patternChanged(track)
6 years ago
5 years ago
#Other functions. These can't be template functions because they use a specific track and Patroneos row and scale system.
6 years ago
def noteOn(trackId, row):
track = session.data.trackById(trackId)
6 years ago
midipitch = track.pattern.scale[row]
6 years ago
cbox.send_midi_event(0x90, midipitch, track.pattern.averageVelocity, output=track.sequencerInterface.cboxMidiOutUuid)
6 years ago
def noteOff(trackId, row):
track = session.data.trackById(trackId)
6 years ago
midipitch = track.pattern.scale[row]
6 years ago
cbox.send_midi_event(0x80, midipitch, track.pattern.averageVelocity, output=track.sequencerInterface.cboxMidiOutUuid)
6 years ago