Browse Source

Sync template

master
Nils 5 years ago
parent
commit
449518b764
  1. 117
      template/engine/api.py
  2. 27
      template/engine/session.py

117
template/engine/api.py

@ -51,20 +51,20 @@ class Callbacks(object):
self.debugChanged = [] #only for testing and debug.
self.setPlaybackTicks = []
self.playbackStatusChanged = []
self.bbtStatusChanged = []
self.barBeatTempo = []
self.clock = []
self.bbtStatusChanged = []
self.barBeatTempo = []
self.clock = []
self.historyChanged = []
self.historySequenceStarted = []
self.historySequenceStopped = []
self.message = []
self.message = []
#Live Midi Recording
self.recordingModeChanged = []
#Sequencer
self.numberOfTracksChanged = []
self.metronomeChanged = []
self.metronomeChanged = []
#Sf2 Sampler
self.soundfontChanged = []
@ -85,7 +85,7 @@ class Callbacks(object):
Insert, delete edit are real data changes. Cursor movement or playback ticks are not."""
session.nsmClient.announceSaveStatus(False)
self._historyChanged()
self._historyChanged()
def _historyChanged(self):
@ -131,7 +131,7 @@ class Callbacks(object):
def _setPlaybackTicks(self):
ppqn = cbox.Transport.status().pos_ppqn
ppqn = cbox.Transport.status().pos_ppqn
status = playbackStatus()
for func in self.setPlaybackTicks:
func(ppqn, status)
@ -143,8 +143,8 @@ class Callbacks(object):
This callback cannot be called manually. Instead it will be called automatically to make
it possible to react to external jack transport changes.
This is deprecated. Append to _checkPlaybackStatusAndSendSignal which is checked by the
This is deprecated. Append to _checkPlaybackStatusAndSendSignal which is checked by the
event loop.
"""
pass #only keep for the docstring and to keep the pattern.
@ -152,9 +152,9 @@ class Callbacks(object):
def _checkPlaybackStatusAndSendSignal(self):
"""Added to the event loop.
We don'T have a jack callback to inform us of this so we drive our own polling system
which in turn triggers our own callback, when needed."""
which in turn triggers our own callback, when needed."""
status = playbackStatus()
if not self._rememberPlaybackStatus == status:
if not self._rememberPlaybackStatus == status:
self._rememberPlaybackStatus = status
for func in self.playbackStatusChanged:
func(status)
@ -163,7 +163,7 @@ class Callbacks(object):
"""Added to the event loop.
We don'T have a jack callback to inform us of this so we drive our own polling system
which in turn triggers our own callback, when needed.
We are interested in:
bar
beat #first index is 1
@ -173,25 +173,25 @@ class Callbacks(object):
beat_type [4.0]
ticks_per_beat [960.0] #JACK ticks, not cbox.
beats_per_minute [120.0]
int bar is the current bar.
int bar is the current bar.
int beat current beat-within-bar
int tick current tick-within-beat
double bar_start_tick number of ticks that have elapsed between frame 0 and the first beat of the current measure.
"""
data = cbox.JackIO.jack_transport_position() #this includes a lot of everchanging data. If no jack-master client set /bar and the others they will simply not be in the list
t = (data.beats_per_bar, data.ticks_per_beat)
if not self._rememberBBT == t: #new situation, but not just frame position update
self._rememberBBT = t
export = {}
if data.beats_per_bar:
double bar_start_tick number of ticks that have elapsed between frame 0 and the first beat of the current measure.
"""
data = cbox.JackIO.jack_transport_position() #this includes a lot of everchanging data. If no jack-master client set /bar and the others they will simply not be in the list
t = (data.beats_per_bar, data.ticks_per_beat)
if not self._rememberBBT == t: #new situation, but not just frame position update
self._rememberBBT = t
export = {}
if data.beats_per_bar:
offset = (data.beat-1) * data.ticks_per_beat + data.tick #if timing is good this is the same as data.tick because beat is 1.
offset = jackBBTicksToDuration(data.beat_type, offset, data.ticks_per_beat)
export["nominator"] = data.beats_per_bar
offset = jackBBTicksToDuration(data.beat_type, offset, data.ticks_per_beat)
export["nominator"] = data.beats_per_bar
export["denominator"] = jackBBTicksToDuration(data.beat_type, data.ticks_per_beat, data.ticks_per_beat) #the middle one is the changing one we are interested in
export["measureInTicks"] = export["nominator"] * export["denominator"]
export["offsetToMeasureBeginning"] = offset
#export["tickposition"] = cbox.Transport.status().pos_ppqn #this is a different position than our current one because it takes a few cycles and ticks to calculate
#export["tickposition"] = cbox.Transport.status().pos_ppqn #this is a different position than our current one because it takes a few cycles and ticks to calculate
export["tickposition"] = cbox.Transport.samples_to_ppqn(data.frame)
for func in self.bbtStatusChanged:
func(export)
@ -199,31 +199,35 @@ class Callbacks(object):
#Send bar beats tempo, for displays
#TODO: broken
"""
bbtExport = {}
if data.beat and not self._rememberBarBeatTempo == data.beat:
bbtExport = {}
if data.beat and not self._rememberBarBeatTempo == data.beat:
bbtExport["timesig"] = f"{int(data.beats_per_bar)}/{int(data.beat_type)}" #for displays
bbtExport["beat"] = data.beat #index from 1
bbtExport["tempo"] = int(data.beats_per_minute)
bbtExport["bar"] = int(data.bar)
self._rememberBarBeatTempo = data.beat #this should be enough inertia to not fire every 100ms
bbtExport["bar"] = int(data.bar)
self._rememberBarBeatTempo = data.beat #this should be enough inertia to not fire every 100ms
for func in self.barBeatTempo:
func(bbtExport)
func(bbtExport)
elif not data.beat:
for func in self.barBeatTempo:
func(bbtExport)
func(bbtExport)
self._rememberBarBeatTempo = data.beat
"""
clock = str(timedelta(seconds=data.frame / data.frame_rate))
clock = str(timedelta(seconds=data.frame / data.frame_rate))
for func in self.clock:
func(clock)
func(clock)
#Live Midi Recording
def _recordingModeChanged(self):
if session.recordingEnabled:
session.nsmClient.changeLabel("Recording")
else:
session.nsmClient.changeLabel("")
for func in self.recordingModeChanged:
func(session.recordingEnabled)
func(session.recordingEnabled)
#Sequencer
def _numberOfTracksChanged(self):
@ -231,7 +235,7 @@ class Callbacks(object):
Sent the current track order as list of ids, combined with their structure.
This is also used when tracks get created or deleted, also on initial load.
This also includes the pattern.
"""
"""
session.data.updateJackMetadataSorting()
lst = [track.export() for track in session.data.tracks]
for func in self.numberOfTracksChanged:
@ -242,12 +246,13 @@ class Callbacks(object):
exportDict = session.data.metronome.export()
for func in self.metronomeChanged:
func(exportDict)
#Sf2 Sampler
def _soundfontChanged(self):
"""User loads a new soundfont or on load. Resets everything."""
exportDict = session.data.export()
session.data.updateAllChannelJackMetadaPrettyname()
session.nsmClient.changeLabel(exportDict["name"])
if exportDict:
for func in self.soundfontChanged:
func(exportDict)
@ -277,25 +282,25 @@ def startEngine(nsmClient):
It gets called by client applications before their own startEngine.
Stopping the engine is done via pythons atexit in the session.
session.eventLoop is overwritten by template.qtgui.mainWindow . It is the first action
it takes after imports.
"""
"""
assert session
assert callbacks
session.nsmClient = nsmClient
session.eventLoop.fastConnect(callbacks._setPlaybackTicks)
session.eventLoop.fastConnect(cbox.get_new_events) #global cbox.get_new_events does not eat dynamic midi port events.
session.eventLoop.fastConnect(cbox.get_new_events) #global cbox.get_new_events does not eat dynamic midi port events.
session.eventLoop.slowConnect(callbacks._checkPlaybackStatusAndSendSignal)
session.eventLoop.slowConnect(callbacks._checkBBTAndSendSignal)
#session.eventLoop.slowConnect(lambda: print(cbox.Transport.status().tempo))
#asession.eventLoop.slowConnect(lambda: print(cbox.Transport.status()))
cbox.Document.get_song().update_playback()
#asession.eventLoop.slowConnect(lambda: print(cbox.Transport.status()))
callbacks._recordingModeChanged()
cbox.Document.get_song().update_playback()
callbacks._recordingModeChanged() #recording mode is in the save file.
callbacks._historyChanged() #send initial undo status to the GUI, which will probably deactivate its undo/redo menu because it is empty.
def _deprecated_updatePlayback():
@ -309,7 +314,7 @@ def _deprecated_updatePlayback():
def save():
"""Saves the file in place. This is mostly here for psychological reasons. Users like to hit
Ctrl+S from muscle memory.
But it can also be used if we run with fake NSM. In any case, it does not accept paths"""
But it can also be used if we run with fake NSM. In any case, it does not accept paths"""
session.nsmClient.serverSendSaveToSelf()
def undo():
@ -333,7 +338,7 @@ def getUndoLists():
#Calfbox Sequencer Controls
def playbackStatus()->bool:
#status = "[Running]" if cbox.Transport.status().playing else "[Stopped]" #it is not that simple.
cboxStatus = cbox.Transport.status().playing
cboxStatus = cbox.Transport.status().playing
if cboxStatus == 1:
#status = "[Running]"
return True
@ -357,7 +362,7 @@ def playPause():
The api, or the session, do not call that.
Playback can be started externally via jack transport.
We use the jack transport callbacks instead and trigger our own callbacks directly from them,
in the callback class above"""
in the callback class above"""
if playbackStatus():
cbox.Transport.stop()
else:
@ -379,7 +384,7 @@ def playFrom(ticks):
seek(ticks)
if not playbackStatus():
cbox.Transport.play()
def playFromStart():
toStart()
if not playbackStatus():
@ -392,12 +397,12 @@ def toggleRecordingMode():
# Sequencer Metronome
def setMetronome(data, label):
session.data.metronome.generate(data, label)
callbacks._metronomeChanged()
callbacks._metronomeChanged()
def enableMetronome(value):
session.data.metronome.setEnabled(value) #has side effects
callbacks._metronomeChanged()
callbacks._metronomeChanged()
def isMetronomeEnabled():
return session.data.metronome.enabled
@ -453,7 +458,7 @@ def setPatch(channel, bank, program):
class TestValues(object):
value = 0
def history_test_change():
def history_test_change():
"""
We simulate a function that gets its value from context.
Here it is random, but it may be the cursor position in a real program."""

27
template/engine/session.py

@ -49,10 +49,10 @@ class Session(object):
self.nsmClient = None #We get it from api.startEngine which gets it from the GUI. nsmClient.reactToMessage is added to the global event loop there.
self.history = History() #Undo and Redo. Works through the api but is saved in the session. Not saved in the save file.
self.guiSharedDataToSave = {} #the gui can write its values here directly to get them saved and restored on startup. We opt not to use the Qt config save to keep everything in one file.
self.recordingEnabled = False #MidiInput callbacks can use this to prevent/allow data creation. Handled via api callback. Not saved.
self.eventLoop = None # added in api.startEngine
self.recordingEnabled = False #MidiInput callbacks can use this to prevent/allow data creation. Handled via api callback. Saved.
self.eventLoop = None # added in api.startEngine
self.data = None #nsm_openOrNewCallback
def addSessionPrefix(self, jsonDataAsString:str):
"""During load the current session prefix gets added. Turning pseudo-relative paths into
@ -100,7 +100,7 @@ class Session(object):
atexit.register(self.stopSession) #this will handle all python exceptions, but not segfaults of C modules.
cbox.do_cmd("/master/set_ppqn_factor", None, [D4]) #quarter note has how many ticks? needs to be in a list.
self.sessionPrefix = ourPath #if we want to save and load resources they need to be in the session dir. We never load from outside, the scheme is always "import first, load local file"
self.absoluteJsonFilePath = os.path.join(ourPath, "save." + METADATA["shortName"] + ".json")
@ -112,7 +112,7 @@ class Session(object):
self.data = None
logger.error("Will not load or save because: " + e.__repr__())
if not self.data:
self.data = Data(parentSession = self)
self.data = Data(parentSession = self)
logger.info("New/Open session complete")
def openFromJson(self, absoluteJsonFilePath):
@ -122,11 +122,13 @@ class Session(object):
text = self.addSessionPrefix(f.read())
result = json.loads(text)
except Exception as error:
logger.error(error)
logger.error(error)
if result and "version" in result and "origin" in result and result["origin"] == METADATA["url"]:
if METADATA["version"] >= result["version"]:
self.guiWasSavedAsNSMVisible = result["guiWasSavedAsNSMVisible"]
if "recordingEnabled" in result: #introduced in april 2020
self.recordingEnabled = result["recordingEnabled"]
self.guiSharedDataToSave = result["guiSharedDataToSave"]
assert type(self.guiSharedDataToSave) is dict, self.guiSharedDataToSave
logger.info("Loading file complete")
@ -137,11 +139,11 @@ class Session(object):
else:
warn(f"""Error. {absoluteJsonFilePath} not loaded. Not a sane {METADATA["name"]} file in json format""")
sysexit()
def nsm_saveCallback(self, ourPath, sessionName, ourClientNameUnderNSM):
#not neccessary. NSMClient does that for us. self.nsmClient.announceSaveStatus(True)
try:
try:
if not os.path.exists(ourPath):
os.makedirs(ourPath)
except Exception as e:
@ -150,6 +152,7 @@ class Session(object):
result = self.data.serialize()
result["origin"] = METADATA["url"]
result["version"] = METADATA["version"]
result["recordingEnabled"] = self.recordingEnabled
result["guiWasSavedAsNSMVisible"] = self.nsmClient.isVisible
result["guiSharedDataToSave"] = self.guiSharedDataToSave
#result["savedOn"] = datetime.now().isoformat() #this is inconvenient for git commits. Even a save without no changes will trigger a git diff.
@ -173,9 +176,9 @@ class Session(object):
self.eventLoop.stop()
logger.info("@atexit: Event loop stopped")
#Don't do that. We are just a client.
#cbox.Transport.stop()
#cbox.Transport.stop()
#logger.info("@atexit: Calfbox Transport stopped ")
cbox.stop_audio()
logger.info("@atexit: Calfbox Audio stopped ")
cbox.shutdown_engine()
cbox.stop_audio()
logger.info("@atexit: Calfbox Audio stopped ")
cbox.shutdown_engine()
logger.info("@atexit: Calfbox Engine shutdown ")

Loading…
Cancel
Save