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.
214 lines
12 KiB
214 lines
12 KiB
#/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
|
|
This is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import logging; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
#Python Standard Library
|
|
import os
|
|
import os.path
|
|
import json
|
|
import atexit
|
|
from sys import exit as sysexit
|
|
import base64 #for jack metadata icon
|
|
|
|
#Third Party Modules
|
|
from template.calfbox import cbox
|
|
|
|
#Our Template Modules
|
|
from .history import History
|
|
from .duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128, MAXIMUM_TICK_DURATION
|
|
from ..start import PATHS
|
|
|
|
#User Data
|
|
from engine.config import *
|
|
from engine.main import Data
|
|
|
|
class Session(object):
|
|
|
|
def __init__(self):
|
|
self.sessionPrefix = "" #set in nsm_openOrNewCallback. 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 = "" #set in nsm_openOrNewCallback. Makes it possible to save a file manually. Even in script mode or debug mode.
|
|
self.guiWasSavedAsNSMVisible = False #set by nsm_openOrNewCallback, but without a save file we start with an initially hidden GUI
|
|
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. Saved.
|
|
self.eventLoop = None # added in api.startEngine
|
|
self.data = None #nsm_openOrNewCallback
|
|
self.standaloneMode = None #fake NSM single server for LaborejoSoftwareSuite programs or not. Set in nsm_openOrNewCallback
|
|
|
|
def addSessionPrefix(self, jsonDataAsString:str):
|
|
"""During load the current session prefix gets added. Turning pseudo-relative paths into
|
|
absolute paths.
|
|
|
|
This is meant for imported resources, e.g. a soundfont, which need to be handled as abolute
|
|
paths while the program is running (e.g. for fluidsynth). It leaves our engine functions
|
|
cleaner because they do not know if they are under session management or not.
|
|
|
|
It assumes that the session dir does not change during runtime, which is a reasonable
|
|
assumption."""
|
|
assert type(jsonDataAsString) is str, type(jsonDataAsString)
|
|
assert self.sessionPrefix, self.sessionPrefix
|
|
return jsonDataAsString.replace("<sessionDirectory>", self.sessionPrefix)
|
|
|
|
def removeSessionPrefix(self, jsonDataAsString:str):
|
|
"""During saving the current session prefix gets removed from all absolute paths leaving
|
|
pseudo-relative paths "<sessionDirectory>" behind.
|
|
|
|
This is meant for imported resources, e.g. a soundfont, which need to be handled as abolute
|
|
paths while the program is running (e.g. for fluidsynth). It leaves our engine functions
|
|
cleaner because they do not know if they are under session management or not.
|
|
|
|
It assumes that the session dir does not change during runtime, which is a reasonable
|
|
assumption."""
|
|
assert type(jsonDataAsString) is str, type(jsonDataAsString)
|
|
return jsonDataAsString.replace(self.sessionPrefix, "<sessionDirectory>")
|
|
|
|
def nsm_openOrNewCallback(self, ourPath, sessionName, ourClientNameUnderNSM):
|
|
logger.info("New/Open session")
|
|
cbox.init_engine("")
|
|
|
|
#Most set config must be called before start audio. Set audio outputs seems to be an exception.
|
|
cbox.Config.set("io", "client_name", ourClientNameUnderNSM)
|
|
cbox.Config.set("io", "enable_common_midi_input", 0) #remove the default "catch all" midi input
|
|
cbox.Config.set("io", "outputs", METADATA["cboxOutputs"])
|
|
|
|
#A workaround to have the program adopt to JACK transport position. It already tries but there is no song yet, so it is song-duration=0 and setting transport is not possible beyond song duration.
|
|
#We fake a song length which will shortly be set to the real size.
|
|
cbox.Document.get_song().set_loop(MAXIMUM_TICK_DURATION, MAXIMUM_TICK_DURATION)
|
|
cbox.Document.get_song().update_playback()
|
|
|
|
#Now we can start audio with a fake song length
|
|
cbox.start_audio()
|
|
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")
|
|
|
|
self.standaloneMode = sessionName == "NOT-A-SESSION"
|
|
|
|
try:
|
|
self.data = self.openFromJson(self.absoluteJsonFilePath)
|
|
except FileNotFoundError:
|
|
self.data = None #This makes debugging output nicer. If we init Data() here all errors will be presented as follow-up error "while handling exception FileNotFoundError".
|
|
except (NotADirectoryError, PermissionError) as e:
|
|
self.data = None
|
|
logger.error("Will not load or save because: " + e.__repr__())
|
|
if not self.data:
|
|
self.data = Data(parentSession = self)
|
|
|
|
self.sendJackMetadataIcon()
|
|
|
|
logger.info("New/Open session complete")
|
|
|
|
def openFromJson(self, absoluteJsonFilePath):
|
|
logger.info("Loading file start")
|
|
with open(absoluteJsonFilePath, "r", encoding="utf-8") as f:
|
|
try:
|
|
text = self.addSessionPrefix(f.read())
|
|
result = json.loads(text)
|
|
except Exception as error:
|
|
logger.error(error)
|
|
|
|
if result and "version" in result and "origin" in result and result["origin"] == METADATA["url"]:
|
|
if METADATA["version"] >= result["version"]: #Achtung. This only works because Python can compare Strings this way!
|
|
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")
|
|
return Data.instanceFromSerializedData(parentSession=self, serializedData=result)
|
|
else:
|
|
logger.error(f"""{absoluteJsonFilePath} was saved with {result["version"]} but we need {METADATA["version"]}""")
|
|
sysexit()
|
|
else:
|
|
logger.error(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:
|
|
if not os.path.exists(ourPath):
|
|
os.makedirs(ourPath)
|
|
except Exception as e:
|
|
logger.error("Will not load or save because: " + e.__repr__())
|
|
|
|
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.
|
|
jsonData = json.dumps(result, indent=2)
|
|
jsonData = self.removeSessionPrefix(jsonData)
|
|
|
|
#if result and jsonData: #make a test here to make sure that the data is not wiped out or corrupted
|
|
try:
|
|
with open(self.absoluteJsonFilePath, "w", encoding="utf-8") as f:
|
|
f.write(jsonData)
|
|
except Exception as e:
|
|
logger.error("Will not load or save because: " + e.__repr__())
|
|
|
|
logger.info("Saving file complete")
|
|
return self.absoluteJsonFilePath
|
|
|
|
def stopSession(self):
|
|
"""This got registered with atexit in the nsm new or open callback above.
|
|
will handle all python exceptions, but not segfaults of C modules. """
|
|
logger.info("Starting Quit through @atexit, session.stopSession")
|
|
self.eventLoop.stop()
|
|
logger.info("@atexit: Event loop stopped")
|
|
#Don't do that. We are just a client.
|
|
#cbox.Transport.stop()
|
|
#logger.info("@atexit: Calfbox Transport stopped ")
|
|
cbox.stop_audio()
|
|
logger.info("@atexit: Calfbox Audio stopped ")
|
|
cbox.shutdown_engine()
|
|
logger.info("@atexit: Calfbox Engine shutdown ")
|
|
|
|
|
|
def sendJackMetadataIcon(self):
|
|
"""Convert our icon to base64 UTF-8 and send it to jack metadata.
|
|
Actually, the icon is already encoded in our codebase so we don't have to locate the file
|
|
on the users disk when installed
|
|
|
|
Sent once at the end of self.nsm_openOrNewCallback
|
|
"""
|
|
#testData = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAAAAABWESUoAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAAAgcAAAIHASw6dZwAAAAHdElNRQfkBBEHMwXdauI0AAABrElEQVQ4y4XTv0vbQRzG8fc3Jv5WqqUKRvjaaKE6pE5RBFHBdlIQSp3yB3QouDh0q05K0UUzuZQMLdh/wFKhKEIERR1EOwlVxGhaUyqRSox5HBI1F5N423Gv4z6fu+fgwaGsEXqaObsPTtx2QXDVT21B8IEq/hQA3x1NY4QygcMoOOx3fX3Ej7xdJPqYVRBf3iPGGZa2sLbzgKWiZ2fSZTn+3CDiLtmQpCGK1nOB5CAzkqRF8MZzgI8MJVOyAybug1WXHZUkxffmoexnNvhnu0KS9PddNQC+RBYYYVKSfnmsDv/rJ8CcCY5LfFeS/nvbdiVdjEJT0gDTLEjSVMVRamMPrBjgVWVCkpobWgd2JCkAIwZw90rSPkDjuaQ96DRe87cNEAY43AQaLQ5TK2lgxQBanIDVABTXEDWAOwLweNIBbz0AcZxGHt6UnkmStma+SZLC0GwU+YlZI3qfYdAAsZr600zQDQHzqqd4eX63HoSKsAkuu+m6jftaGbzPfu7j59ROpCr9Ug3tsTSwULqbyPAyTu8L18XSAdSt2ekLyshkIlB/07rnNtcPf/5rhItvET2iDPMAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDQtMTdUMDc6NTE6MDUrMDA6MDAudbalAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA0LTE3VDA3OjUxOjA1KzAwOjAwXygOGQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAASUVORK5CYII="
|
|
|
|
logger.info("Sending program icons to jack metadata")
|
|
|
|
icon_32_base64_utf8 = os.path.join(PATHS["share"], "icon_32_base64_utf8.txt")
|
|
assert os.path.exists(icon_32_base64_utf8), icon_32_base64_utf8
|
|
with open(icon_32_base64_utf8, "r") as fSmall:
|
|
cbox.JackIO.Metadata.set_icon_small(fSmall.read())
|
|
|
|
icon_128_base64_utf8 = os.path.join(PATHS["share"], "icon_128_base64_utf8.txt")
|
|
assert os.path.exists(icon_128_base64_utf8), icon_128_base64_utf8
|
|
with open(icon_128_base64_utf8, "r") as fLarge:
|
|
cbox.JackIO.Metadata.set_icon_large(fLarge.read())
|
|
|
|
cbox.JackIO.Metadata.set_icon_name(METADATA["shortName"])
|
|
|