Sampled Instrument Player with static and monolithic design. All instruments are built-in.
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

#/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" #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"])