Browse Source

Add advanced feature for a global rhythm offset, meant for Jack-Transport. Allows the whole piece to begin later on the jack-timeline.

master
Nils 3 years ago
parent
commit
f631ea4556
  1. 1
      CHANGELOG
  2. 38
      engine/api.py
  3. 36
      engine/main.py
  4. 4
      engine/track.py
  5. 53
      qtgui/mainwindow.py
  6. 1985
      qtgui/resources.py
  7. BIN
      qtgui/resources/translations/de.qm
  8. 187
      qtgui/resources/translations/de.ts
  9. 122
      qtgui/submenus.py
  10. 3
      template/engine/api.py

1
CHANGELOG

@ -3,6 +3,7 @@ Full Undo/Redo
Add option to change the midi channel for a track in the tracks context menu.
Add switch group places to song-structure context menus.
Add View-Menu with options to maximize the two editor areas.
Add advanced feature for a global rhythm offset, meant for Jack-Transport. Allows the whole piece to begin later on the jack-timeline.
2021-01-15 Version 2.0.0
Big new features increase the MAJOR version. Old save files can still be loaded.

38
engine/api.py

@ -219,6 +219,18 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
func(_swingToPercent_Table[export])
callbacks._dataChanged()
#Override template one
def _setPlaybackTicks(self):
"""This gets called very very often (~60 times per second).
Any connected function needs to watch closely
for performance issues"""
ppqn = cbox.Transport.status().pos_ppqn - session.data.cachedOffsetInTicks
status = playbackStatus()
for func in self.setPlaybackTicks:
func(ppqn, status)
self._dataChanged() #includes _historyChanged
#Inject our derived Callbacks into the parent module
template.engine.api.callbacks = ClientCallbacks()
from template.engine.api import callbacks
@ -269,23 +281,27 @@ def _loopNow():
def _setLoop(loopMeasureAroundPpqn:int):
"""This function is used with context.
The loopFactor, how many measures are looped, is saved value """
if loopMeasureAroundPpqn < 0:
_loopOff()
return
loopStart, loopEnd = session.data.buildSongDuration(loopMeasureAroundPpqn)
#loopMeasureAroundPpqn = max(0, loopMeasureAroundPpqn + session.data.cachedOffsetInTicks)
loopMeasureAroundPpqn = loopMeasureAroundPpqn - session.data.cachedOffsetInTicks
loopStart, loopEnd = session.data.buildSongDuration(loopMeasureAroundPpqn) #includes global tick offset
session.data._lastLoopStart = loopStart
updatePlayback()
session.inLoopMode = (loopStart, loopEnd)
assert loopStart <= loopMeasureAroundPpqn < loopEnd
assert loopStart <= loopMeasureAroundPpqn+session.data.cachedOffsetInTicks < loopEnd, (loopStart, loopMeasureAroundPpqn, loopEnd)
if not playbackStatus():
cbox.Transport.play()
oneMeasureInTicks = (session.data.howManyUnits * session.data.whatTypeOfUnit) / session.data.subdivisions
measurenumber, rest = divmod(loopStart, oneMeasureInTicks)
measurenumber, rest = divmod(loopStart-session.data.cachedOffsetInTicks, oneMeasureInTicks) #We substract the offset from the measure number because for a GUI it is still the visible measure number
callbacks._loopChanged(int(measurenumber), loopStart, loopEnd)
@ -320,8 +336,24 @@ def seek(value):
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()
value = max(0, value + session.data.cachedOffsetInTicks)
cbox.Transport.seek_ppqn(value)
def getGlobalOffset():
"""Return the current offsets in full measures + free tick value 3rd: Cached abolute tick value
gets updated everytime the time signature changes or setGlobalOffset is called"""
return session.data.globalOffsetMeasures, session.data.globalOffsetTicks, session.data.cachedOffsetInTicks
def setGlobalOffset(fullMeasures, absoluteTicks):
session.history.register(lambda f=session.data.globalOffsetMeasures, t=session.data.globalOffsetTicks: setGlobalOffset(f,t), descriptionString="Global Rhythm Offset")
session.data.globalOffsetMeasures = fullMeasures
session.data.globalOffsetTicks = absoluteTicks
session.data.buildAllTracks() #includes refreshing the tick offset cache
updatePlayback()
##Score
def set_quarterNotesPerMinute(value):
"""Transport Master is set implicitly. If value == None Patroneo will switch into

36
engine/main.py

@ -59,6 +59,10 @@ class Data(template.engine.sequencer.Score):
self.loopMeasureFactor = 1 #when looping how many at once?
self.swing = 0 #-0.5 to 0.5 See pattern.buildPattern docstring.
self.globalOffsetMeasures = 0 #relative to the current measure length. Parsed every export.
self.globalOffsetTicks = 0 #absolute
self.cachedOffsetInTicks = 0
#Create three tracks with their first pattern activated, so 'play' after startup already produces sounding notes. This is less confusing for a new user.
self.addTrack(name="Melody A", color="#ffff00")
self.addTrack(name="Chords A", color="#0055ff")
@ -79,6 +83,12 @@ class Data(template.engine.sequencer.Score):
def _processAfterInit(self):
self._lastLoopStart = 0 #ticks. not saved
def cacheOffsetInTicks(self):
oneMeasureInTicks = (self.howManyUnits * self.whatTypeOfUnit) / self.subdivisions
oneMeasureInTicks = int(oneMeasureInTicks)
self.cachedOffsetInTicks = oneMeasureInTicks * self.globalOffsetMeasures + self.globalOffsetTicks
return self.cachedOffsetInTicks
def addTrack(self, name="", scale=None, color=None, simpleNoteNames=None):
"""Overrides the simpler template version"""
track = Track(parentData=self, name=name, scale=scale, color=color, simpleNoteNames=simpleNoteNames)
@ -161,6 +171,8 @@ class Data(template.engine.sequencer.Score):
If True it will reset the loop. The api calls buildSongDuration directly when it sets
the loop.
"""
self.cacheOffsetInTicks()
for track in self.tracks:
track.pattern.buildExportCache()
track.buildTrack()
@ -174,15 +186,18 @@ class Data(template.engine.sequencer.Score):
An optional loopMeasureFactor can loop more than one measure. This is especially useful is
track multiplicators are used. This is a saved context value in self.
"""
self.cacheOffsetInTicks()
oneMeasureInTicks = (self.howManyUnits * self.whatTypeOfUnit) / self.subdivisions
oneMeasureInTicks = int(oneMeasureInTicks)
maxTrackDuration = self.numberOfMeasures * oneMeasureInTicks
maxTrackDuration = self.numberOfMeasures * oneMeasureInTicks + self.cachedOffsetInTicks
if loopMeasureAroundPpqn is None: #could be 0
cbox.Document.get_song().set_loop(maxTrackDuration, maxTrackDuration) #set playback length for the entire score. Why is the first value not zero? That would create an actual loop from the start to end. We want the song to play only once. The cbox way of doing that is to set the loop range to zero at the end of the track. Zero length is stop.
else:
loopMeasure = int(loopMeasureAroundPpqn / oneMeasureInTicks) #0 based
start = loopMeasure * oneMeasureInTicks
end = start + oneMeasureInTicks * self.loopMeasureFactor
start = start + self.cachedOffsetInTicks
end = end + self.cachedOffsetInTicks
cbox.Document.get_song().set_loop(start, end) #set playback length for the entire score. Why is the first value not zero? That would create an actual loop from the start to end. We want the song to play only once. The cbox way of doing that is to set the loop range to zero at the end of the track. Zero length is stop.
return start, end
@ -199,6 +214,8 @@ class Data(template.engine.sequencer.Score):
"lastUsedNotenames" : self.lastUsedNotenames,
"loopMeasureFactor" : self.loopMeasureFactor,
"swing" : self.swing,
"globalOffsetMeasures" : self.globalOffsetMeasures,
"globalOffsetTicks" : self.globalOffsetTicks,
})
return dictionary
@ -211,15 +228,26 @@ class Data(template.engine.sequencer.Score):
self.measuresPerGroup = serializedData["measuresPerGroup"]
self.subdivisions = serializedData["subdivisions"]
self.lastUsedNotenames = serializedData["lastUsedNotenames"]
if "loopMeasureFactor" in serializedData: #2.0
#v2.0
if "loopMeasureFactor" in serializedData:
self.loopMeasureFactor = serializedData["loopMeasureFactor"]
else:
self.loopMeasureFactor = 1
if "swing" in serializedData: #2.0
if "swing" in serializedData:
self.swing = serializedData["swing"]
else:
self.swing = 0
#v2.1
if "globalOffsetMeasures" in serializedData:
self.globalOffsetMeasures = serializedData["globalOffsetMeasures"]
else:
self.globalOffsetMeasures = 0
if "globalOffsetTicks" in serializedData:
self.globalOffsetTicks = serializedData["globalOffsetTicks"]
else:
self.globalOffsetTicks = 0
#Tracks depend on the rest of the data already in place because they create a cache on creation.
super().copyFromSerializedData(parentSession, serializedData, self) #Tracks, parentSession and tempoMap
@ -237,6 +265,8 @@ class Data(template.engine.sequencer.Score):
"loopMeasureFactor" : self.loopMeasureFactor,
"isTransportMaster" : self.tempoMap.export()["isTransportMaster"],
"swing" : self.swing,
"globalOffsetMeasures" : self.globalOffsetMeasures,
"globalOffsetTicks" : self.globalOffsetTicks,
}

4
engine/track.py

@ -83,13 +83,15 @@ class Track(object): #injection at the bottom of this file!
filteredStructure = [index for index in sorted(self.structure) if index < self.parentData.numberOfMeasures] #not <= because we compare count with range
cboxclips = [o.clip for o in self.sequencerInterface.calfboxTrack.status().clips]
globalOffset = self.parentData.cachedOffsetInTicks
for cboxclip in cboxclips:
cboxclip.delete() #removes itself from the track
for index in filteredStructure:
scaleTransposition = self.whichPatternsAreScaleTransposed[index] if index in self.whichPatternsAreScaleTransposed else 0
halftoneTransposition = self.whichPatternsAreHalftoneTransposed[index] if index in self.whichPatternsAreHalftoneTransposed else 0
cboxPattern = self.pattern.buildPattern(scaleTransposition, halftoneTransposition, self.parentData.howManyUnits, self.parentData.whatTypeOfUnit, self.parentData.subdivisions, self.patternLengthMultiplicator)
r = self.sequencerInterface.calfboxTrack.add_clip(index*oneMeasureInTicks, 0, oneMeasureInTicks, cboxPattern) #pos, pattern-internal offset, length, pattern.
r = self.sequencerInterface.calfboxTrack.add_clip(globalOffset + index*oneMeasureInTicks, 0, oneMeasureInTicks, cboxPattern) #pos, pattern-internal offset, length, pattern.
######Old optimisations. Keep for later####
##########################################

53
qtgui/mainwindow.py

@ -37,6 +37,7 @@ from .songeditor import SongEditor, TrackLabelEditor
from .timeline import Timeline
from .pattern_grid import PatternGrid, VelocityControls, TransposeControls
from .resources import *
from .submenus import convertSubdivisionsSubMenu, globalOffsetSubmenu
MAX_QT_SIZE = 2147483647-1
@ -88,6 +89,7 @@ class MainWindow(TemplateMainWindow):
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Change Row Velocity")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Change Pattern Velocity")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Number of Notes in Pattern")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Global Rhythm Offset")
@ -546,50 +548,8 @@ class MainWindow(TemplateMainWindow):
self.ui.timelineView.scale(self.zoomFactor, 1)
#view.centerOn(event.scenePos())
def convertSubdivisionsSubMenu(self):
class Submenu(QtWidgets.QDialog):
def __init__(self, mainWindow):
super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it.
#self.setModal(True) #we don't need this when called with self.exec() instead of self.show()
self.layout = QtWidgets.QFormLayout()
self.setLayout(self.layout)
self.numberOfSubdivisions = QtWidgets.QSpinBox()
self.numberOfSubdivisions.setMinimum(1)
self.numberOfSubdivisions.setMaximum(4)#
self.numberOfSubdivisions.setValue(mainWindow.numberOfSubdivisions.value())
self.layout.addRow(QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "New Grouping"), self.numberOfSubdivisions)
self.errorHandling = QtWidgets.QComboBox()
self.errorHandling.addItems([
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Do nothing"),
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Delete wrong steps"),
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Merge wrong steps"),
])
self.layout.addRow(QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "If not possible"), self.errorHandling)
#self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons.
buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
self.layout.addWidget(buttonBox)
def __call__(self):
"""This instance can be called like a function"""
self.exec() #blocks until the dialog gets closed
s = Submenu(self)
s()
if s.result():
value = s.numberOfSubdivisions.value()
error= ("fail", "delete", "merge")[s.errorHandling.currentIndex()]
api.convert_subdivisions(value, error)
def _stretchXCoordinates(*args): pass #Override template function
def maximizeSongArea(self):
self.ui.patternArea.setMinimumSize(1, 1)
self.ui.splitter.setSizes([MAX_QT_SIZE, 1])
@ -607,13 +567,8 @@ class MainWindow(TemplateMainWindow):
#self.ui.actionUndo.setVisible(False)
#self.ui.actionRedo.setVisible(False)
self.menu.addMenuEntry(
"menuEdit",
"actionConvertSubdivisions",
QtCore.QCoreApplication.translate("Menu", "Convert Grouping"),
self.convertSubdivisionsSubMenu,
tooltip=QtCore.QCoreApplication.translate("Menu", "Change step-grouping but keep your music the same"),
)
self.menu.addMenuEntry("menuEdit", "actionConvertSubdivisions", QtCore.QCoreApplication.translate("Menu", "Convert Grouping"), lambda: convertSubdivisionsSubMenu(self), tooltip=QtCore.QCoreApplication.translate("Menu", "Change step-grouping but keep your music the same"))
self.menu.addMenuEntry("menuEdit", "actionGlobalOffsetSubmenu", QtCore.QCoreApplication.translate("Menu", "Global Rhythm Offset"), lambda: globalOffsetSubmenu(self), tooltip=QtCore.QCoreApplication.translate("Menu", "Shift the whole piece further down the timeline"))
self.menu.addSubmenu("menuView", QtCore.QCoreApplication.translate("menu","View"))
self.menu.addMenuEntry("menuView", "actionMaximizeSongArea", QtCore.QCoreApplication.translate("menu", "Maximize Song Area"), self.maximizeSongArea)

1985
qtgui/resources.py

File diff suppressed because it is too large

BIN
qtgui/resources/translations/de.qm

Binary file not shown.

187
qtgui/resources/translations/de.ts

@ -4,32 +4,32 @@
<context>
<name>About</name>
<message>
<location filename="../../mainwindow.py" line="110"/>
<location filename="../../mainwindow.py" line="112"/>
<source>Prefer clone track over adding a new empty track when creating a new pattern for an existing &apos;real world&apos; instrument.</source>
<translation>Spuren zu klonen ist meist besser als eine neue, leere Spur zu erstellen. Benutze Klonen immer wenn du ein neues Pattern für ein existierendes Instrument komponieren möchtest.</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="111"/>
<location filename="../../mainwindow.py" line="113"/>
<source>You can run multiple Patroneo instances in parallel to create complex polyrhythms.</source>
<translation>Um komplexe Rhythmen zu erstellen versuche Patroneo mehrmals zu starten und verschiedene Taktarten einzustellen.</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="112"/>
<location filename="../../mainwindow.py" line="114"/>
<source>To revert all steps that are longer or shorter than default invert the pattern twice in a row.</source>
<translation>Alle gedehnten oder verkürzte Noten im Takt bekommst du am einfachsten zurück auf die normale Länge wenn du zweimal hintereinander die &amp;quot;Umkehren&amp;quot; Funktion benutzt.</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="113"/>
<location filename="../../mainwindow.py" line="115"/>
<source>Control a synth with MIDI Control Changes (CC) by routing a Patroneo track into a midi plugin that converts notes to CC.</source>
<translation>MIDI Control Changes (CC) werden nicht direkt von Patroneo erzeugt. Route eine Extraspur in ein Konverterplugin, dass aus Pitch und Velocity CC und Value macht.</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="114"/>
<location filename="../../mainwindow.py" line="116"/>
<source>The mouse wheel is very powerful: Use it to transpose measures (with or without Shift pressed), it resizes the measure number line, zooms when Ctrl is held down, changes row volumes in the pattern with the Alt key or sounds a preview if pressed on a step.</source>
<translation>Das Mausrad ist sehr wichtig: Es transponiert Takte (mit oder ohne Umschalttaste), verändert die Größe der Taktgruppen, zoomed wenn Strg gedrückt ist, ändert die Lautstärke einer ganzen Reihe zusammen mit der Alt-Taste oder lässt eine Note erklingen wenn man es auf einem Schritt drückt.</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="115"/>
<location filename="../../mainwindow.py" line="117"/>
<source>Many elements have context menus with unique functions: Try right clicking on a Step, the Track name or a measure in the song editor.</source>
<translation>Die meisten Bedienelemente haben ein Kontextmenü. Versuche auf alles mit der rechten Maustaste zu klicken: Schritte, Takte, der Spurname etc.</translation>
</message>
@ -93,263 +93,278 @@
<context>
<name>Menu</name>
<message>
<location filename="../../mainwindow.py" line="610"/>
<location filename="../../mainwindow.py" line="570"/>
<source>Convert Grouping</source>
<translation>Gruppierung umwandeln</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="610"/>
<location filename="../../mainwindow.py" line="570"/>
<source>Change step-grouping but keep your music the same</source>
<translation>Taktartaufspaltung durch Gruppierung umwandeln, versucht die Musik gleich klingen zu lassen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="571"/>
<source>Global Rhythm Offset</source>
<translation>Allgemeiner Rhythmusversatz</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="571"/>
<source>Shift the whole piece further down the timeline</source>
<translation>Schiebt das gesamte Stück &quot;später&quot; auf die Timeline</translation>
</message>
</context>
<context>
<name>NOOPengineHistory</name>
<message>
<location filename="../../mainwindow.py" line="46"/>
<location filename="../../mainwindow.py" line="47"/>
<source>Tempo</source>
<translation>Tempo</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="47"/>
<location filename="../../mainwindow.py" line="48"/>
<source>Group Duration</source>
<translation>Gruppierungsdauer</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="48"/>
<location filename="../../mainwindow.py" line="49"/>
<source>Steps per Pattern</source>
<translation>Schritte pro Takt</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="49"/>
<location filename="../../mainwindow.py" line="50"/>
<source>Group Size</source>
<translation>Gruppengröße</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="50"/>
<location filename="../../mainwindow.py" line="51"/>
<source>Convert Grouping</source>
<translation>Gruppierung umwandeln</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="51"/>
<location filename="../../mainwindow.py" line="52"/>
<source>Swing</source>
<translation>Swing</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="52"/>
<location filename="../../mainwindow.py" line="53"/>
<source>Measures per Track</source>
<translation>Takte pro Spur</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="53"/>
<location filename="../../mainwindow.py" line="54"/>
<source>Measures per Group</source>
<translation>Takte pro Gruppe</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="54"/>
<location filename="../../mainwindow.py" line="55"/>
<source>Track Name</source>
<translation>Spurname</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="55"/>
<location filename="../../mainwindow.py" line="56"/>
<source>Track Color</source>
<translation>Spurfarbe</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="56"/>
<location filename="../../mainwindow.py" line="57"/>
<source>Track Midi Channel</source>
<translation>Spur Midikanal</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="57"/>
<location filename="../../mainwindow.py" line="58"/>
<source>Add Track</source>
<translation>Neue Spur</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="58"/>
<location filename="../../mainwindow.py" line="59"/>
<source>Clone Track</source>
<translation>Spur klonen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="59"/>
<location filename="../../mainwindow.py" line="60"/>
<source>Add deleted Track again</source>
<translation>Gelöschte Spur wieder hinzufügen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="60"/>
<location filename="../../mainwindow.py" line="61"/>
<source>Delete Track and autocreated Track</source>
<translation>Lösche Spur und automatische Ersatzspur</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="61"/>
<location filename="../../mainwindow.py" line="62"/>
<source>Delete Track</source>
<translation>Spur löschen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="62"/>
<location filename="../../mainwindow.py" line="63"/>
<source>Move Track</source>
<translation>Spur bewegen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="63"/>
<location filename="../../mainwindow.py" line="64"/>
<source>Pattern Multiplier</source>
<translation>Takt-Skalierung</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="64"/>
<location filename="../../mainwindow.py" line="65"/>
<source>Set Measures</source>
<translation>Setze Takte</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="65"/>
<location filename="../../mainwindow.py" line="66"/>
<source>Invert Measures</source>
<translation>Taktauswahl umdrehen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="66"/>
<location filename="../../mainwindow.py" line="67"/>
<source>Track Measures Off</source>
<translation>Alle Takte ausschalten</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="67"/>
<location filename="../../mainwindow.py" line="68"/>
<source>Track Measures On</source>
<translation>Alle Takte einschalten</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="68"/>
<location filename="../../mainwindow.py" line="69"/>
<source>Copy Measures</source>
<translation>Takte Kopieren</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="69"/>
<location filename="../../mainwindow.py" line="70"/>
<source>Replace Measures</source>
<translation>Takte Ersetzen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="70"/>
<location filename="../../mainwindow.py" line="71"/>
<source>Set Modal Shift</source>
<translation>Modale Transposition</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="71"/>
<location filename="../../mainwindow.py" line="72"/>
<source>Set Half Tone Shift</source>
<translation>Halbtontransposition</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="72"/>
<location filename="../../mainwindow.py" line="73"/>
<source>Change Group</source>
<translation>Gruppe verändern</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="73"/>
<location filename="../../mainwindow.py" line="74"/>
<source>Insert/Duplicate Group</source>
<translation>Gruppe einfügen/duplizieren</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="75"/>
<location filename="../../mainwindow.py" line="76"/>
<source>Clear all Group Transpositions</source>
<translation>Alle Transpositionen der Taktgruppe zurücksetzen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="76"/>
<location filename="../../mainwindow.py" line="77"/>
<source>Delete whole Group</source>
<translation>Taktgruppe Löschen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="77"/>
<location filename="../../mainwindow.py" line="78"/>
<source>Change Step</source>
<translation>Schritt verändern</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="78"/>
<location filename="../../mainwindow.py" line="79"/>
<source>Remove Step</source>
<translation>Schritt aus</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="79"/>
<location filename="../../mainwindow.py" line="80"/>
<source>Set Scale</source>
<translation>Benutze Tonleiter</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="80"/>
<location filename="../../mainwindow.py" line="81"/>
<source>Note Names</source>
<translation>Notennamen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="81"/>
<location filename="../../mainwindow.py" line="82"/>
<source>Transpose Scale</source>
<translation>Tonleiter transponieren</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="82"/>
<location filename="../../mainwindow.py" line="83"/>
<source>Invert Steps</source>
<translation>Schritte invertieren</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="83"/>
<location filename="../../mainwindow.py" line="84"/>
<source>All Steps On</source>
<translation>Alle Schritte an</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="84"/>
<location filename="../../mainwindow.py" line="85"/>
<source>All Steps Off</source>
<translation>Alles Schritte aus</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="85"/>
<location filename="../../mainwindow.py" line="86"/>
<source>Invert Row</source>
<translation>Reihe umkehren</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="86"/>
<location filename="../../mainwindow.py" line="87"/>
<source>Clear Row</source>
<translation>Reihe löschen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="87"/>
<location filename="../../mainwindow.py" line="88"/>
<source>Fill Row with Repeat</source>
<translation>Reihenwiederholung</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="88"/>
<location filename="../../mainwindow.py" line="89"/>
<source>Change Row Velocity</source>
<translation>Reihenlautstärke</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="89"/>
<location filename="../../mainwindow.py" line="90"/>
<source>Change Pattern Velocity</source>
<translation>Taktlautstärke</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="90"/>
<location filename="../../mainwindow.py" line="91"/>
<source>Number of Notes in Pattern</source>
<translation>Anzahl der Tonhöhen im Takt</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="74"/>
<location filename="../../mainwindow.py" line="75"/>
<source>Exchange Group Order</source>
<translation>Gruppenreihenfolge tauschen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="92"/>
<source>Global Rhythm Offset</source>
<translation>Allgemeiner Rhythmusversatz</translation>
</message>
</context>
<context>
<name>PlaybackControls</name>
<message>
<location filename="../../mainwindow.py" line="131"/>
<location filename="../../mainwindow.py" line="133"/>
<source>[Space] Play / Pause</source>
<translation>[Leertaste] Play / Pause</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="138"/>
<location filename="../../mainwindow.py" line="140"/>
<source>[L] Loop current Measure</source>
<translation>[L] Aktueller Takt in Schleife spielen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="159"/>
<location filename="../../mainwindow.py" line="161"/>
<source>[Home] Jump to Start</source>
<translation>[Pos1] Springe zum Anfang</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="152"/>
<location filename="../../mainwindow.py" line="154"/>
<source>Number of measures in the loop</source>
<translation>Anzahl der Takte pro Schleife</translation>
</message>
@ -468,27 +483,27 @@
<context>
<name>TimeSignature</name>
<message>
<location filename="../../mainwindow.py" line="398"/>
<location filename="../../mainwindow.py" line="400"/>
<source>Whole</source>
<translation>Ganze</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="399"/>
<location filename="../../mainwindow.py" line="401"/>
<source>Half</source>
<translation>Halbe</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="400"/>
<location filename="../../mainwindow.py" line="402"/>
<source>Quarter</source>
<translation>Viertel</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="401"/>
<location filename="../../mainwindow.py" line="403"/>
<source>Eigth</source>
<translation>Achtel</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="402"/>
<location filename="../../mainwindow.py" line="404"/>
<source>Sixteenth</source>
<translation>Sechzehntel</translation>
</message>
@ -504,32 +519,32 @@
<context>
<name>Toolbar</name>
<message>
<location filename="../../mainwindow.py" line="311"/>
<location filename="../../mainwindow.py" line="313"/>
<source>BPM/Tempo: </source>
<translation>BPM/Tempo: </translation>
</message>
<message>
<location filename="../../mainwindow.py" line="312"/>
<location filename="../../mainwindow.py" line="314"/>
<source>Deactivate to beccome JACK Transport Slave. Activate for Master.</source>
<translation>Aus: JACK Transport Slave. An: JACK Master (eigenes Tempo).</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="365"/>
<location filename="../../mainwindow.py" line="367"/>
<source>Overall length of the song</source>
<translation>Länge des Stückes in Takten</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="382"/>
<location filename="../../mainwindow.py" line="384"/>
<source>Please read the manual!</source>
<translation>Bitte im Handbuch nachlesen!</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="409"/>
<location filename="../../mainwindow.py" line="411"/>
<source>Length of the pattern (bottom part of the program)</source>
<translation>Länge des Musters in Schritten (untere Hälfte des Programms)</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="413"/>
<location filename="../../mainwindow.py" line="415"/>
<source>How long is each main step</source>
<translation>Welchen Notenwert repräsentiert ein Schritt</translation>
</message>
@ -544,47 +559,47 @@
<translation type="obsolete">Taktartaufspaltung durch Gruppierung umwandeln, versucht die Musik gleich klingen zu lassen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="487"/>
<location filename="../../mainwindow.py" line="489"/>
<source>Clone Selected Track</source>
<translation>Klone ausgewählte Spur</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="488"/>
<location filename="../../mainwindow.py" line="490"/>
<source>Use this! Create a new track that inherits everything but the content from the original. Already jack connected!</source>
<translation>Das hier benutzen! Neue Spur, die alle Eigenschaften außer der Musik selbst vom Original erbt. Ist bereits in JACK verbunden!</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="489"/>
<location filename="../../mainwindow.py" line="491"/>
<source>Add Track</source>
<translation>Neue Spur</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="490"/>
<location filename="../../mainwindow.py" line="492"/>
<source>Add a complete empty track that needs to be connected to an instrument manually.</source>
<translation>Eine neue, leere Spur, bei der man noch per Hand ein Instrument mit JACK verbinden muss.</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="495"/>
<location filename="../../mainwindow.py" line="497"/>
<source>Measures per Track: </source>
<translation>Takte pro Spur: </translation>
</message>
<message>
<location filename="../../mainwindow.py" line="501"/>
<location filename="../../mainwindow.py" line="503"/>
<source>Steps per Pattern:</source>
<translation>Schritte pro Takt:</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="506"/>
<location filename="../../mainwindow.py" line="508"/>
<source> in groups of: </source>
<translation> gruppiert in je: </translation>
</message>
<message>
<location filename="../../mainwindow.py" line="511"/>
<location filename="../../mainwindow.py" line="513"/>
<source> so that each group produces a:</source>
<translation> und jede Gruppe ergibt eine:</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="517"/>
<location filename="../../mainwindow.py" line="519"/>
<source>Set the swing factor. 0 is off. Different effect for different rhythm-grouping!</source>
<translation>Swing-Anteil. 0 ist aus. Andere rhythmische Gruppierungen haben einen anderen Effekt!</translation>
</message>
@ -751,48 +766,48 @@
<message>
<location filename="../../mainwindow.py" line="561"/>
<source>New Grouping</source>
<translation>Neue Gruppierung</translation>
<translation type="obsolete">Neue Gruppierung</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="564"/>
<source>Do nothing</source>
<translation>Nichts machen</translation>
<translation type="obsolete">Nichts machen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="564"/>
<source>Delete wrong steps</source>
<translation>Falsche Schritte löschen</translation>
<translation type="obsolete">Falsche Schritte löschen</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="564"/>
<source>Merge wrong steps</source>
<translation>Falsche Schritte mit einbinden</translation>
<translation type="obsolete">Falsche Schritte mit einbinden</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="569"/>
<source>If not possible</source>
<translation>Falls unmöglich</translation>
<translation type="obsolete">Falls unmöglich</translation>
</message>
</context>
<context>
<name>menu</name>
<message>
<location filename="../../mainwindow.py" line="618"/>
<location filename="../../mainwindow.py" line="573"/>
<source>View</source>
<translation>Ansicht</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="619"/>
<location filename="../../mainwindow.py" line="574"/>
<source>Maximize Song Area</source>
<translation>Formeditor maximieren</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="620"/>
<location filename="../../mainwindow.py" line="575"/>
<source>Maximize Pattern Area</source>
<translation>Takteditor maximieren</translation>
</message>
<message>
<location filename="../../mainwindow.py" line="621"/>
<location filename="../../mainwindow.py" line="576"/>
<source>Equal space for Pattern/Song Area</source>
<translation>Gleiche Größe für Form- und Takteditor</translation>
</message>

122
qtgui/submenus.py

@ -0,0 +1,122 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
This application 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")
#Standard Library Modules
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui
#Our modules
import engine.api as api
from template.engine.duration import D4
MAX_QT_SIZE = 2147483647-1
def convertSubdivisionsSubMenu(mainWindow):
class Submenu(QtWidgets.QDialog):
def __init__(self, mainWindow):
super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it.
#self.setModal(True) #we don't need this when called with self.exec() instead of self.show()
self.layout = QtWidgets.QFormLayout()
self.setLayout(self.layout)
self.numberOfSubdivisions = QtWidgets.QSpinBox()
self.numberOfSubdivisions.setMinimum(1)
self.numberOfSubdivisions.setMaximum(4)#
self.numberOfSubdivisions.setValue(mainWindow.numberOfSubdivisions.value())
self.layout.addRow(QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "New Grouping"), self.numberOfSubdivisions)
self.errorHandling = QtWidgets.QComboBox()
self.errorHandling.addItems([
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Do nothing"),
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Delete wrong steps"),
QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "Merge wrong steps"),
])
self.layout.addRow(QtCore.QCoreApplication.translate("convertSubdivisionsSubMenu", "If not possible"), self.errorHandling)
#self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons.
buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
self.layout.addWidget(buttonBox)
def __call__(self):
"""This instance can be called like a function"""
self.exec() #blocks until the dialog gets closed
s = Submenu(mainWindow)
s()
if s.result():
value = s.numberOfSubdivisions.value()
error= ("fail", "delete", "merge")[s.errorHandling.currentIndex()]
api.convert_subdivisions(value, error)
def globalOffsetSubmenu(mainWindow):
class Submenu(QtWidgets.QDialog):
def __init__(self, mainWindow):
super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it.
#self.setModal(True) #we don't need this when called with self.exec() instead of self.show()
self.layout = QtWidgets.QFormLayout()
self.setLayout(self.layout)
quarterticks = D4
offsetMeasures, offsetTicks, completeOffsetinTicks = api.getGlobalOffset()
explanationLabel = QtWidgets.QLabel(f"Current Offset in Ticks: {completeOffsetinTicks}\n\nFor advanced users who are using multiple music programs with JackTransport-Sync.\n\nHere you can set a global rhythm offset to let Patroneo 'wait' before it's playback.\nThe shift will be invisible in the GUI.\n\nFull Measures are current Patroneo measures. If you change their duration the offset will change as well.\n\nThe 'Ticks' value is absolute. A quarter note has {quarterticks} ticks.\n\nYou can give even give negative numbers, but it impossible to shift before position 0. As an advanced user this is your responsibility :)")
explanationLabel.setWordWrap(True)
self.layout.addRow(explanationLabel) #only one parameter will span both columns
self.offsetMeasures = QtWidgets.QSpinBox()
self.offsetMeasures.setMinimum(-1 * MAX_QT_SIZE)
self.offsetMeasures.setMaximum(MAX_QT_SIZE)
self.offsetMeasures.setValue(offsetMeasures)
self.offsetTicks = QtWidgets.QSpinBox()
self.offsetTicks.setMinimum(-1 * MAX_QT_SIZE)
self.offsetTicks.setMaximum(MAX_QT_SIZE)
self.offsetTicks.setValue(offsetTicks)
self.layout.addRow(QtCore.QCoreApplication.translate("globalOffsetSubmenu", "Full Patroneo Measures"), self.offsetMeasures)
self.layout.addRow(QtCore.QCoreApplication.translate("globalOffsetSubmenu", "Ticks (Quarter=96)"), self.offsetTicks)
#self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons.
buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
self.layout.addWidget(buttonBox)
def __call__(self):
"""This instance can be called like a function"""
self.exec() #blocks until the dialog gets closed
s = Submenu(mainWindow)
s()
if s.result():
api.setGlobalOffset(s.offsetMeasures.value(), s.offsetTicks.value())

3
template/engine/api.py

@ -130,7 +130,8 @@ class Callbacks(object):
def _setPlaybackTicks(self):
"""This gets called very very often. Any connected function needs to watch closely
"""This gets called very very often (~60 times per second).
Any connected function needs to watch closely
for performance issues"""
ppqn = cbox.Transport.status().pos_ppqn
status = playbackStatus()

Loading…
Cancel
Save