#/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 . """ 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("", self.sessionPrefix) def removeSessionPrefix(self, jsonDataAsString:str): """During saving the current session prefix gets removed from all absolute paths leaving pseudo-relative paths "" 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, "") 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" #this is a bool! not a chain variable creation. The second is a double == 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"])