|
|
|
#! /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 <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import logging; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
|
|
|
|
#Python Standard Lib
|
|
|
|
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
|
|
|
|
|
|
|
|
basePath = pathlib.Path("/home/nils/samples/Tembro/out/")
|
|
|
|
for f in basePath.glob('*.tar'):
|
|
|
|
if f.is_file() and f.suffix == ".tar":
|
|
|
|
lib = Library(parentData=self, tarFilePath=f)
|
|
|
|
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.
|
|
|
|
|
|
|
|
There is also a shortcut. First only an external .ini is loaded, which is much faster than
|
|
|
|
unpacking the tar. If no additional data like images are needed (which is always the case
|
|
|
|
at this version 1.0) we parse this external ini directly to build our database.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, parentData, tarFilePath):#
|
|
|
|
self.parentData = parentData
|
|
|
|
self.tarFilePath = tarFilePath #pathlib.Path()
|
|
|
|
if not tarFilePath.suffix == ".tar":
|
|
|
|
raise RuntimeError(f"Wrong file {tarFilePath}")
|
|
|
|
|
|
|
|
|
|
|
|
needTarData = False #TODO: If we have images etc. in the future.
|
|
|
|
if needTarData:
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
else: #the default case for now.
|
|
|
|
iniName = str(tarFilePath)[:-4] + ".ini"
|
|
|
|
self.config = configparser.ConfigParser()
|
|
|
|
self.config.read(iniName)
|
|
|
|
|
|
|
|
self.id = self.config["library"]["id"]
|
|
|
|
instrumentSections = self.config.sections()
|
|
|
|
instrumentSections.remove("library")
|
|
|
|
instrumentsInLibraryCount = len(instrumentSections)
|
|
|
|
self.instruments = {} # instrId : Instrument()
|
|
|
|
for iniSection in instrumentSections:
|
|
|
|
instrObj = Instrument(self, self.config["library"]["id"], self.config[iniSection], tarFilePath)
|
|
|
|
instrObj.instrumentsInLibraryCount = instrumentsInLibraryCount
|
|
|
|
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
|
|
|
|
|
|
|
|
|