#! /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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Python Standard Lib import os.path import configparser import pathlib import tarfile from io import TextIOWrapper #Third Party from calfbox import cbox #Template Modules from template.engine.data import Data as TemplateData from template.start import PATHS #Our Modules from engine.instrument import Instrument from engine.auditioner import Auditioner class Data(TemplateData): """There must always be a Data class in a file main.py. The main data is in: self.instruments= {} # (libraryId, instrumentId):Instrument()-object This is created on program startup and never modified afterwards (except internal instrument changes of course). Throughout the program we identify instruments with these unique values: * libraryId : integer, no zero-padding. One for each tar file. * instrumentId : integer, no zero-padding. Unique only within a tar file. * variant: string. An .sfz file name. Can use all characters allowed as linux file name, including spaces. Case sensitive. """ def __init__(self, parentSession): #Program start. super().__init__(parentSession) session = self.parentSession #Create two mixer ports, for stereo. Each instrument will not only create their own jack out ports #but also connect to these left/right. assert not session.standaloneMode is None if session.standaloneMode: self.lmixUuid = cbox.JackIO.create_audio_output('left_mix', "#1") #add "#1" as second parameter for auto-connection to system out 1 self.rmixUuid = cbox.JackIO.create_audio_output('right_mix', "#2") #add "#2" as second parameter for auto-connection to system out 2 else: self.lmixUuid = cbox.JackIO.create_audio_output('left_mix') self.rmixUuid = cbox.JackIO.create_audio_output('right_mix') #Auditioner: Create an additional stereo port pair to pre-listen to on sample instrument alone self.auditioner = Auditioner(self) #Parse all tar libraries and load them all. ALL OF THEM! #for each library in ... self.libraries = {} # libraryId:int : Library-object lib = Library(parentData=self, tarFilePath="/home/nils/lss/test-data.tar") self.libraries[lib.id] = lib def exportMetadata(self)->dict: """Data we sent in callbacks. This is the initial 'build-the-instrument-database' function. Each first level dict contains another dict with instruments, but also a special key "library" that holds the metadata for the lib itself. """ result = {} for libId, libObj in self.libraries.items(): result[libId] = libObj.exportMetadata() #also a dict. Contains a special key "library" which holds the library metadata itself return result class Library(object): """Open a .tar library and extract information without actually loading any samples. This is for GUI data etc. You get all metadata from this. The samples are not loaded when Library() returns. The API can loop over Instrument.allInstruments and call instr.loadSamples() and send a feedback to callbacks. """ def __init__(self, parentData, tarFilePath):# self.parentData = parentData self.tarFilePath = pathlib.Path(tarFilePath) if not tarFilePath.endswith(".tar"): raise RuntimeError(f"Wrong file {tarFilePath}") with tarfile.open(name=tarFilePath, mode='r:') as opentarfile: iniFileObject = TextIOWrapper(opentarfile.extractfile("library.ini")) self.config = configparser.ConfigParser() self.config.read_file(iniFileObject) #self.config is permant now. We can close the file object """ #Extract an image file. But only if it exists. tarfile.getmember is basically an exist-check that trows KeyError if not try: imageAsBytes = extractfile("logo.png").read() #Qt can handle the format except KeyError: #file not found imageAsBytes = None """ self.id = self.config["library"]["id"] instrumentSections = self.config.sections() instrumentSections.remove("library") self.instruments = {} # instrId : Instrument() for iniSection in instrumentSections: instrObj = Instrument(self, self.config["library"]["id"], self.config[iniSection], tarFilePath) self.instruments[self.config[iniSection]["id"]] = instrObj #At a later point Instrument.loadSamples() must be called. This is done in the API. def exportMetadata(self)->dict: """Return a dictionary with each key is an instrument id, but also a special key "library" with our own metadata. Allows the callbacks receiver to construct a hierarchy""" result = {} libDict = {} result["library"] = libDict #Explicit is better than implicit assert self.config["library"]["id"] == self.id, (self.config["library"]["id"], self.id) libDict["tarFilePath"] = self.tarFilePath libDict["id"] = self.config["library"]["id"] libDict["name"] = self.config["library"]["name"] libDict["description"] = self.config["library"]["description"] libDict["license"] = self.config["library"]["license"] libDict["vendor"] = self.config["library"]["vendor"] for instrument in self.instruments.values(): result[instrument.id] = instrument.exportMetadata() #another dict return result