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.

159 lines
6.3 KiB

3 years ago
#! /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 os.path
import configparser
import pathlib
import tarfile
from io import TextIOWrapper
3 years ago
#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
3 years ago
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.
3 years ago
"""
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