示例#1
0
    def __init__(self, mmv_shader_maker):

        # Get master class
        self.mmv_shader_maker = mmv_shader_maker
        self.transformations = self.mmv_shader_maker.transformations

        self.utils = Utils()
示例#2
0
    def __init__(self, context, config: dict, skia_object) -> None:

        debug_prefix = "[MMVPianoRoll.__init__]"

        self.context = context
        self.config = config
        self.skia = skia_object

        self.utils = Utils()

        self.is_deletable = False
        self.offset = [0, 0]

        self.midi = MidiFile()
        self.midi.load(self.context.input_midi, bpm=self.config["bpm"])
        self.midi.get_timestamps()

        # We have different files with different classes of PianoRolls

        # Simple, rectangle bar
        if self.config["type"] == "top-down":
            self.builder = MMVPianoRollTopDown(self, self.context, self.skia,
                                               self.midi)

        self.builder.generate_piano(self.midi.range_notes.min,
                                    self.midi.range_notes.max)
示例#3
0
    def __init__(self, context, config: dict, skia_object) -> None:
        
        debug_prefix = "[MMVMusicBars.__init__]"
        
        self.context = context
        self.config = config
        self.skia = skia_object

        self.fit_transform_index = FitIndex()
        self.functions = Functions()
        self.utils = Utils()

        self.path = {}

        self.x = 0
        self.y = 0
        self.size = 1
        self.is_deletable = False
        self.offset = [0, 0]
        self.polar = PolarCoordinates()

        self.current_fft = {}

        self.image = Frame()

        # We use separate file and classes for each type of visualizer

        # Circle, radial visualizer
        if self.config["type"] == "circle":
            self.builder = MMVMusicBarsCircle(self, self.context, self.skia)
示例#4
0
    def __init__(self, interface, depth=LOG_NO_DEPTH):
        debug_prefix = "[MMVShaderMain.__init__]"
        ndepth = depth + LOG_NEXT_DEPTH
        self.interface = interface

        self.MMV_SHADER_ROOT = self.interface.MMV_SHADER_ROOT
        self.prelude = self.interface.prelude
        self.os = self.interface.os

        # # Create classes

        logging.info(f"{depth}{debug_prefix} Creating Utils")
        logging.info(STEP_SEPARATOR)
        self.utils = Utils()

        logging.info(f"{depth}{debug_prefix} Creating MMVShaderContext")
        logging.info(STEP_SEPARATOR)
        self.context = MMVShaderContext(self)

        logging.info(f"{depth}{debug_prefix} Creating MMVShaderMPV")
        logging.info(STEP_SEPARATOR)
        self.mpv = MMVShaderMPV(self)

        logging.info(f"{depth}{debug_prefix} Creating MMVShaderMaker")
        logging.info(STEP_SEPARATOR)
        self.shader_maker = MMVShaderMaker(self)
class LAVector():
    def __init__(self):
        self.utils = Utils()

    # Build the coordinates from a given list of coordinates
    def from_coordinates(self, coordinates):
        self.coordinates = coordinates

    # Build the vector based on two points
    def from_two_points(self, A, B):
        # To get the direction and orientation of a vector based on two points, we have:
        # AB = B - A
        # So the result of B - A = (b1 - a1, b2 - a2, b3 - a3..., bn - an)
        if self.utils.is_matching_type([A, B], [LAPoint, LAPoint]):
            if len(A.coordinates) == len(B.coordinates):
                self.coordinates = [
                    B.coordinates[i] - A.coordinates[i]
                    for i, _ in enumerate(A.coordinates)
                ]
            else:
                print("[LA POINT ERROR] Two points with different spaces")
        else:
            print("[LA VECTOR ERROR] Arguments aren't two LAPoint types")

    # Calculate the magnitude of a vector
    def magnitude(self):
        # Sum every square of the components and get the root of it
        return (sum([x**2 for x in self.coordinates]))**0.5
    def __init__(self, context, config: dict, skia_object) -> None:

        debug_prefix = "[MMVMusicBars.__init__]"

        self.context = context
        self.config = config
        self.skia = skia_object

        self.utils = Utils()

        self.path = {}

        self.x = 0
        self.y = 0
        self.size = 1
        self.is_deletable = False
        self.offset = [0, 0]

        self.image = Frame()

        # We have different files with different classes of ProgressionBars

        # Simple, rectangle bar
        if self.config["type"] == "rectangle":
            self.builder = MMVProgressionBarRectangle(self, self.context,
                                                      self.skia)
示例#7
0
    def __init__(self, mmv, **kwargs) -> None:
        
        debug_prefix = "[MMVSkiaProgressionBarVectorial.__init__]"
        
        self.mmv = mmv
        self.config = {}

        self.utils = Utils()

        self.path = {}

        self.x = 0
        self.y = 0
        self.size = 1
        self.is_deletable = False
        self.offset = [0, 0]

        self.image = Frame()

        # General Configuration

        self.config["type"] = kwargs.get("bar_type", "rectangle")
        self.config["position"] = kwargs.get("position", "bottom")
        self.config["shake_scalar"] = kwargs.get("shake_scalar", 14)

        # We have different files with different classes of ProgressionBars

        # Simple, rectangle bar
        if self.config["type"] == "rectangle":
            print(debug_prefix, "Builder is MMVSkiaProgressionBarRectangle")
            self.builder = MMVSkiaProgressionBarRectangle(self, self.mmv)
        
        else:
            raise RuntimeError(debug_prefix, "No valid builder set, kwargs:", kwargs, "config:", self.config)
    def __init__(self, profile, width, height, quiet=False):

        # Load the profile and a config
        self.profile = profile(width, height)
        self.width = width
        self.height = height
        self.quiet = quiet

        # Create classes
        self.utils = Utils()

        # Empty / "static" variables
        # self.unique_string = ""
        self.nodes = []
        self.ROOT = self.utils.get_root()

        # Create a empty canvas
        self.new_canvas()
示例#9
0
    def __init__(self, mmv, **kwargs) -> None:

        debug_prefix = "[MMVSkiaPianoRollVectorial.__init__]"

        self.mmv = mmv
        self.config = {}

        self.utils = Utils()

        self.is_deletable = False
        self.offset = [0, 0]

        # # General configuration

        self.config["bpm"] = kwargs["bpm"]
        self.config["type"] = kwargs.get("type", "top-down")

        # MMVSkiaPianoRollTopDown
        if self.config["type"] == "top-down":
            self.config["colors"] = kwargs["colors"]
            self.config["do_draw_markers"] = kwargs.get(
                "do_draw_markers", True)
            self.config["bleed"] = kwargs.get("bleed", 3)
            self.config["seconds_of_midi_content"] = kwargs.get(
                "seconds_of_midi_content", 3)
            self.config["seconds_offset"] = kwargs.get("seconds_offset", 0)
            self.config["piano_key_follows_note_color"] = kwargs.get(
                "piano_key_follows_note_color", True)
            self.config["rounding"] = kwargs["rounding"]
            self.config["draw_note_name"] = kwargs["draw_note_name"]

        else:
            raise RuntimeError(
                "No matching MMVSkiaPianoRollVectorial type, kwargs:", kwargs)

        # # Load MIDI file, get timestamps

        print(debug_prefix, "Loading the MIDI file")
        self.midi = MidiFile()
        self.midi.load(self.mmv.context.input_midi, bpm=self.config["bpm"])

        print(debug_prefix, "Getting notes timestamps")
        self.midi.get_timestamps()

        # We have different files with different classes of PianoRolls

        # Simple, rectangle bar
        if self.config["type"] == "top-down":
            print(debug_prefix, "Piano roll class is MMVSkiaPianoRollTopDown")
            self.builder = MMVSkiaPianoRollTopDown(self.mmv, self)

        print(debug_prefix, "Generating Piano Roll")
        self.builder.generate_piano(self.midi.range_notes.min,
                                    self.midi.range_notes.max)
示例#10
0
    def __init__(self, watch_processing_video_realtime: bool = False) -> None:

        # Main class of MMV
        self.mmv = MMVMain()

        # Utilities
        self.utils = Utils()

        # Start MMV classes that main connects them, do not run
        self.mmv.setup()

        # Default options of performance and quality, 720p60
        self.quality()

        # Configuring options
        self.quality_preset = QualityPreset(self)
        self.audio_processing = AudioProcessingPresets(self)
        self.post_processing = self.mmv.canvas.configure

        # Has the user chosen to watch the processing video realtime?
        self.mmv.context.watch_processing_video_realtime = watch_processing_video_realtime
示例#11
0
class MMVShaderMakerLoaders:
    def __init__(self, mmv_shader_maker):

        # Get master class
        self.mmv_shader_maker = mmv_shader_maker
        self.transformations = self.mmv_shader_maker.transformations

        self.utils = Utils()

    def from_yaml(self, path):
        debug_prefix = "[MMVShaderMakerLoaders.from_yaml]"

        logging.info(f"{debug_prefix} Loading config from YAML file [{path}]")
        data = self.utils.load_yaml(path)
    def __init__(self, workers=4):

        # Create Utils and get this file location
        self.utils = Utils()
        self.config = Config(self)
        self.main = PyGradienterMain()

        # Default values
        self.n_images = 1
        self.width = 200
        self.height = 200
        self.show_welcome_message = True
        self.quiet = False
        self.workers = workers
示例#13
0
    def __init__(self, working_directory, name = None, replaces = {}):
        debug_prefix = "[MMVShaderMaker.__init__]"

        # Instantiate classes
        self.utils = Utils()
        self.block_of_code = BlockOfCode

        # Refactors
        self.transformations = MMVShaderMakerTransformations()
        self.loaders = MMVShaderMakerLoaders(mmv_shader_maker = self)
        self.macros = MMVShaderMakerMacros(mmv_shader_maker = self)
    
        # Where to place directories and whatnot
        self.working_directory = working_directory

        # # Sessions and Shader Maker runtime directory

        # Reset runtime directory
        logging.info(f"{debug_prefix} Resetting directory")
        self.utils.reset_dir(self.working_directory)

        # # Add stuff

        # Attributes
        self._includes = []
        self._mappings = []
        self._functions = []
        self._transformations = []

        # Get and add name mapping
        self.name = name
        self.replaces = replaces
     
        # Start with the base shader
        self._fragment_shader = BASE_FRAGMENT_SHADER
        self._final_shader = None
        self._path_on_disk = None
示例#14
0
    def setup(self) -> None:
        debug_prefix = "[MMVSkiaMain.setup]"

        self.utils = Utils()

        logging.info(f"{debug_prefix} Creating MMVContext() class")
        self.context = MMVContext(mmv_skia_main=self)

        logging.info(f"{debug_prefix} Creating SkiaNoWindowBackend() class")
        self.skia = SkiaNoWindowBackend()

        logging.info(f"{debug_prefix} Creating Functions() class")
        self.functions = Functions()

        logging.info(f"{debug_prefix} Creating Interpolation() class")
        self.interpolation = Interpolation()

        logging.info(f"{debug_prefix} Creating PolarCoordinates() class")
        self.polar_coordinates = PolarCoordinates()

        logging.info(f"{debug_prefix} Creating Canvas() class")
        self.canvas = MMVSkiaImage(mmvskia_main=self)

        logging.info(f"{debug_prefix} Creating Fourier() class")
        self.fourier = Fourier()

        # The user must explicitly set and override this, mostly for compatibility
        # and code cleanup reasons.
        self.pipe_video_to = None

        logging.info(f"{debug_prefix} Creating AudioFile() class")
        self.audio = AudioFile()

        logging.info(f"{debug_prefix} Creating AudioProcessing() class")
        self.audio_processing = AudioProcessing()

        logging.info(f"{debug_prefix} Creating MMVSkiaAnimation() class")
        self.mmv_skia_animation = MMVSkiaAnimation(mmv_skia_main=self)

        logging.info(f"{debug_prefix} Creating MMVSkiaCore() class")
        self.core = MMVSkiaCore(mmvskia_main=self)
示例#15
0
    def __init__(self, mmv, **kwargs) -> None:
        self.mmv = mmv

        debug_prefix = "[MMVSkiaMusicBars.__init__]"

        self.kwargs = kwargs

        self.functions = Functions()
        self.utils = Utils()

        self.path = {}

        self.x = 0
        self.y = 0
        self.size = 1
        self.is_deletable = False
        self.offset = [0, 0]
        self.polar = PolarCoordinates()

        self.current_fft = {}

        self.image = Frame()

        # Configuration

        self.kwargs["fourier"] = {
            "interpolation":
            MMVSkiaInterpolation(
                self.mmv,
                function="remaining_approach",
                ratio=self.kwargs.get("bar_responsiveness", 0.25),
            ),
        }

        # # We use separate file and classes for each type of visualizer

        # Circle, radial visualizer
        if self.kwargs["type"] == "circle":
            self.builder = MMVSkiaMusicBarsCircle(self.mmv, self,
                                                  **self.kwargs)
    def setup(self) -> None:

        debug_prefix = "[MMVMain.__init__]"

        self.utils = Utils()

        print(debug_prefix, "Creating Context()")
        self.context = Context(self)

        print(debug_prefix, "Creating SkiaNoWindowBackend()")
        self.skia = SkiaNoWindowBackend(self)

        print(debug_prefix, "Creating Functions()")
        self.functions = Functions()

        print(debug_prefix, "Creating Interpolation()")
        self.interpolation = Interpolation()

        print(debug_prefix, "Creating Canvas()")
        self.canvas = MMVImage(self)

        print(debug_prefix, "Creating Fourier()")
        self.fourier = Fourier()

        print(debug_prefix, "Creating FFmpegWrapper()")
        self.ffmpeg = FFmpegWrapper(self)

        print(debug_prefix, "Creating AudioFile()")
        self.audio = AudioFile()

        print(debug_prefix, "Creating AudioProcessing()")
        self.audio_processing = AudioProcessing()

        print(debug_prefix, "Creating MMVAnimation()")
        self.mmv_animation = MMVAnimation(self)
    
        print(debug_prefix, "Creating Core()")
        self.core = Core(self)
示例#17
0
    def __init__(self, platform=None, **kwargs) -> None:
        debug_prefix = "[MMVPackageInterface.__init__]"

        # Versioning
        self.version = "2.6"

        # Can only run on Python 64 bits, this expression returns 32 if 32 bit installation
        # and 64 if 64 bit installation, we assert that (assume it's true, quit if it isn't)
        assert (struct.calcsize("P") * 8) == 64, (
            "You don't have an 64 bit Python installation, MMV will not work on 32 bit Python "
            "because skia-python package only distributes 64 bit Python wheels (bundles).\n"
            "This is out of my control, Skia devs don't release 32 bit version of Skia anyways\n\n"
            "See issue [https://github.com/kyamagu/skia-python/issues/21]")

        # # Get this file's path

        sep = os.path.sep

        # Where this file is located, please refer using this on the whole package
        # Refer to it as self.mmv_skia_main.MMV_PACKAGE_ROOT at any depth in the code
        # This deals with the case we used pyinstaller and it'll get the executable path instead
        if getattr(sys, 'frozen', True):
            self.MMV_PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__))
            print(f"{debug_prefix} Running directly from source code")
            print(
                f"{debug_prefix} Modular Music Visualizer Python package [__init__.py] located at [{self.MMV_PACKAGE_ROOT}]"
            )
        else:
            self.MMV_PACKAGE_ROOT = os.path.dirname(
                os.path.abspath(sys.executable))
            print(f"{debug_prefix} Running from release (sys.executable..?)")
            print(
                f"{debug_prefix} Modular Music Visualizer executable located at [{self.MMV_PACKAGE_ROOT}]"
            )

        # # Load prelude configuration

        print(f"{debug_prefix} Loading prelude configuration file")

        # Build the path the prelude file should be located at
        prelude_file = f"{self.MMV_PACKAGE_ROOT}{sep}prelude.toml"

        print(
            f"{debug_prefix} Attempting to load prelude file located at [{prelude_file}], we cannot continue if this is wrong.."
        )

        # Load the prelude file
        with open(prelude_file, "r") as f:
            self.prelude = toml.loads(f.read())

        print(
            f"{debug_prefix} Loaded prelude configuration file, data: [{self.prelude}]"
        )

        # # # Logging

        # # We can now set up logging as we have where this file is located at

        # # Reset current handlers if any

        print(
            f"{debug_prefix} Resetting Python's logging logger handlers to empty list"
        )

        # Get logger and empty the list
        logger = logging.getLogger()
        logger.handlers = []

        # Handlers on logging to file and shell output, the first one if the user says to
        handlers = [logging.StreamHandler(sys.stdout)]

        # Loglevel is defined in the prelude.toml configuration
        LOG_LEVEL = {
            "critical": logging.CRITICAL,
            "debug": logging.DEBUG,
            "error": logging.ERROR,
            "info": logging.INFO,
            "warn": logging.WARN,
            "notset": logging.NOTSET,
        }.get(self.prelude["logging"]["log_level"])

        # If user chose to log to a file, add its handler..
        if self.prelude["logging"]["log_to_file"]:

            # Hard coded where the log file will be located
            # this is only valid for the last time we run this software
            self.LOG_FILE = f"{self.MMV_PACKAGE_ROOT}{sep}last_log.log"

            # Reset the log file
            with open(self.LOG_FILE, "w") as f:
                print(
                    f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]"
                )
                f.write("")

            # Verbose and append the file handler
            print(
                f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]")
            handlers.append(
                logging.FileHandler(filename=self.LOG_FILE, encoding='utf-8'))

        # .. otherwise just keep the StreamHandler to stdout

        log_format = {
            "informational":
            "[%(levelname)-8s] [%(filename)-32s:%(lineno)-3d] (%(relativeCreated)-6d) %(message)s",
            "pretty":
            "[%(levelname)-8s] (%(relativeCreated)-5d)ms %(message)s",
            "economic":
            "[%(levelname)s::%(filename)s::%(lineno)d] %(message)s",
            "onlymessage": "%(message)s"
        }.get(self.prelude["logging"]["log_format"])

        # Start the logging global class, output to file and stdout
        logging.basicConfig(
            level=LOG_LEVEL,
            format=log_format,
            handlers=handlers,
        )

        # Greeter message :)
        self.greeter_message()

        # Start logging message
        bias = " " * ((self.terminal_width // 2) - 13)
        print(f"{bias[:-1]}# # [ Start Logging ] # #\n")
        print("-" * self.terminal_width + "\n")

        # Log what we'll do next
        logging.info(
            f"{debug_prefix} We're done with the pre configuration of Python's behavior and loading prelude.toml configuration file"
        )

        # Log precise Python version
        sysversion = sys.version.replace("\n", " ").replace("  ", " ")
        logging.info(f"{debug_prefix} Running on Python: [{sysversion}]")

        # # # FIXME: Python 3.9, go home you're drunk

        # Max python version, show info, assert, pretty print
        maximum_working_python_version = (3, 8)
        pversion = sys.version_info

        # Log and check
        logging.info(
            f"{debug_prefix} Checking if Python <= {maximum_working_python_version} for a working version.. "
        )

        # Huh we're on Python 2..?
        if pversion[0] == 2:
            logging.error(
                f"{debug_prefix} Please upgrade to at least Python 3")
            sys.exit(-1)

        # Python is ok
        if (pversion[0] <= maximum_working_python_version[0]) and (
                pversion[1] <= maximum_working_python_version[1]):
            logging.info(f"{debug_prefix} Ok, good python version")
        else:
            # Warn Python 3.9 is a bit unstable, even the developer had issues making it work
            logging.warn(
                f"{debug_prefix} Python 3.9 is acting a bit weird regarding some dependencies on some systems, while it should be possible to run, take it with some grain of salt and report back into the discussions troubles or workarounds you found?"
            )
            input("\n [ Press enter to continue.. ]: ")

        # # The operating system we're on, one of "linux", "windows", "macos"

        # Get the desired name from a dict matching against os.name
        if platform is None:
            self.os = {
                "posix": "linux",
                "nt": "windows",
                "darwin": "macos"
            }.get(os.name)
        else:
            logging.info(
                f"{debug_prefix} Overriding platform OS to = [{platform}]")
            self.os = platform

        # Log which OS we're running
        logging.info(
            f"{debug_prefix} Running Modular Music Visualizer on Operating System: [{self.os}]"
        )
        logging.info(f"{debug_prefix} (os.path.sep) is [{sep}]")

        # # Create interface's classes

        logging.info(f"{debug_prefix} Creating Utils() class")
        self.utils = Utils()

        logging.info(f"{debug_prefix} Creating Download() class")
        self.download = Download()

        # # Common directories between packages

        # Externals
        self.externals_dir = f"{self.MMV_PACKAGE_ROOT}{sep}externals"
        logging.info(f"{debug_prefix} Externals dir is [{self.externals_dir}]")
        self.utils.mkdir_dne(path=self.externals_dir, silent=True)

        # Downloads (inside externals)
        self.downloads_dir = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}downloads"
        logging.info(f"{debug_prefix} Downloads dir is [{self.downloads_dir}]")
        self.utils.mkdir_dne(path=self.downloads_dir, silent=True)

        # Data dir
        self.data_dir = f"{self.MMV_PACKAGE_ROOT}{sep}data"
        logging.info(f"{debug_prefix} Data dir is [{self.data_dir}]")
        self.utils.mkdir_dne(path=self.data_dir, silent=True)

        # Windoe juuuust in case
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Appending the Externals directory to system path juuuust in case..."
            )
            sys.path.append(self.externals_dir)

        # # Common files

        self.last_session_info_file = f"{self.data_dir}{sep}last_session_info.toml"
        logging.info(
            f"{debug_prefix} Last session info file is [{self.last_session_info_file}], resetting it.."
        )

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

        # # External dependencies where to append for PATH

        # Externals directory for Linux
        self.externals_dir_linux = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}linux"
        logging.info(
            f"{debug_prefix} Externals directory for Linux OS is [{self.externals_dir_linux}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_linux, silent=True)

        # Externals directory for Windows
        self.externals_dir_windows = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}windows"
        logging.info(
            f"{debug_prefix} Externals directory for Windows OS is [{self.externals_dir_windows}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_windows, silent=True)

        # Externals directory for macOS
        self.externals_dir_macos = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}macos"
        logging.info(
            f"{debug_prefix} Externals directory for Darwin OS (macOS) is [{self.externals_dir_macos}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_macos, silent=True)

        # # This native platform externals dir
        self.externals_dir_this_platform = self.__get_platform_external_dir(
            self.os)
        logging.info(
            f"{debug_prefix} This platform externals directory is: [{self.externals_dir_this_platform}]"
        )

        # Update the externals search path (create one in this case)
        self.update_externals_search_path()

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)
 def __init__(self):
     self.utils = Utils()
class LinearAlgebra:
    def __init__(self):
        self.utils = Utils()

    # Distance between two objects
    def distance(self, A, B):
        # Distance between two points
        if self.utils.is_matching_type([A, B], [LAPoint, LAPoint]):
            if len(A.coordinates) == len(B.coordinates):
                return (sum([(A.coordinates[i] - B.coordinates[i])**2
                             for i, _ in enumerate(A.coordinates)]))**0.5
            else:
                print(
                    "[LA ERROR] Dot product between vectors of different sizes: [%s] and [%s]"
                    % A.coordinates, B.coordinates)

    # Dot product between two LAVectors
    def dot_product(self, A, B):
        if self.utils.is_matching_type([A, B], [LAVector, LAVector]):
            if len(A.coordinates) == len(B.coordinates):
                # Sum the multiplication of x1.x2.x3..xK + y1.y2.y3..yK + z1.z2.z3..zK +.. n1.n2.n3..nK
                return sum([
                    A.coordinates[i] * B.coordinates[i]
                    for i, _ in enumerate(A.coordinates)
                ])
            else:
                print(
                    "[LA ERROR] Dot product between vectors of different sizes: [%s] and [%s]"
                    % A.coordinates, B.coordinates)
        else:
            print(
                "[LA ERROR] Can only calculate dot product between two vectors"
            )

    # Get the angle between two vectors in radians
    def angle_between_two_vectors(self, A, B):
        if self.utils.is_matching_type([A, B], [LAVector, LAVector]):
            # The formula is ths one:
            # cos(angle) = a.b / |a||b|
            # We then just have to calculate angle = cos-1 ( a.b / |a||b| )
            # Where . is dot product and || the magnitude of a vector

            multiplied_magnitude = (A.magnitude() * B.magnitude())

            if not multiplied_magnitude == 0:

                # Get the cosine of the angle
                cos_angle = self.dot_product(A, B) / multiplied_magnitude

                # Rounding errors
                if cos_angle < -1:
                    cos_angle = -1
                elif cos_angle > 1:
                    cos_angle = 1

                # Return the angle itself got from cos-1
                return math.acos(cos_angle)
            else:
                return 0
        else:
            print(
                "[LA ERROR] Can only calculate angle between two vectors in this function"
            )

    # B is the mid point, so get the angle between the two vectors: BA and BC
    def angle_between_three_points(self, A, B, C):
        if self.utils.is_matching_type([A, B, C], [LAPoint, LAPoint, LAPoint]):

            # Create two vector objects
            va = LAVector()
            vb = LAVector()

            # Build them with the points
            va.from_two_points(B, A)
            vb.from_two_points(B, C)

            # Get the angle between two vectors
            angle = self.angle_between_two_vectors(va, vb)
            return angle
        else:
            print(
                "[LA ERROR] Can only calculate angle between two vectors in this function"
            )
示例#20
0
    def __init__(self, platform=None, **kwargs) -> None:
        debug_prefix = "[MMVPackageInterface.__init__]"

        self.version = "3.2: rolling"

        # Where this file is located, please refer using this on the whole package
        # Refer to it as self.mmv_skia_main.MMV_PACKAGE_ROOT at any depth in the code
        # This deals with the case we used pyinstaller and it'll get the executable path instead
        if getattr(sys, 'frozen', True):
            self.MMV_PACKAGE_ROOT = Path(
                os.path.dirname(os.path.abspath(__file__)))
            print(f"{debug_prefix} Running directly from source code")
        else:
            self.MMV_PACKAGE_ROOT = Path(
                os.path.dirname(os.path.abspath(sys.executable)))
            print(f"{debug_prefix} Running from release (sys.executable..?)")

        print(
            f"{debug_prefix} Modular Music Visualizer Python package [__init__.py] or executable located at [{self.MMV_PACKAGE_ROOT}]"
        )

        # # Load prelude configuration

        print(f"{debug_prefix} Loading prelude configuration file")

        # Build the path the prelude file should be located at
        prelude_file = self.MMV_PACKAGE_ROOT / "prelude.toml"
        print(
            f"{debug_prefix} Attempting to load prelude file located at [{prelude_file}], we cannot continue if this is wrong.."
        )

        # Load the prelude file
        with open(prelude_file, "r") as f:
            self.prelude = toml.loads(f.read())

        print(
            f"{debug_prefix} Loaded prelude configuration file, data: [{self.prelude}]"
        )

        # # # Logging

        # # We can now set up logging as we have where this file is located at

        # # Reset current handlers if any
        print(
            f"{debug_prefix} Resetting Python's logging logger handlers to empty list"
        )

        # Get logger and empty the list
        logger = logging.getLogger()
        logger.handlers = []

        # Handlers on logging to file and shell output, the first one if the user says to
        handlers = [logging.StreamHandler(sys.stdout)]

        # Loglevel is defined in the prelude.toml configuration
        LOG_LEVEL = {
            "critical": logging.CRITICAL,
            "debug": logging.DEBUG,
            "error": logging.ERROR,
            "info": logging.INFO,
            "warn": logging.warning,
            "notset": logging.NOTSET,
        }.get(self.prelude["logging"]["log_level"])

        # If user chose to log to a file, add its handler..
        if self.prelude["logging"]["log_to_file"]:

            # Hard coded where the log file will be located
            # this is only valid for the last time we run this software
            self.LOG_FILE = self.MMV_PACKAGE_ROOT / "last_log.log"

            # Reset the log file
            with open(self.LOG_FILE, "w") as f:
                print(
                    f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]"
                )
                f.write("")

            # Verbose and append the file handler
            print(
                f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]")
            handlers.append(
                logging.FileHandler(filename=self.LOG_FILE, encoding='utf-8'))

        # .. otherwise just keep the StreamHandler to stdout

        log_format = {
            "informational":
            "[%(levelname)-8s] [%(filename)-32s:%(lineno)-3d] (%(relativeCreated)-6d) %(message)s",
            "pretty":
            "[%(levelname)-8s] (%(relativeCreated)-5d)ms %(message)s",
            "economic":
            "[%(levelname)s::%(filename)s::%(lineno)d] %(message)s",
            "onlymessage": "%(message)s"
        }.get(self.prelude["logging"]["log_format"])

        # Start the logging global class, output to file and stdout
        logging.basicConfig(
            level=LOG_LEVEL,
            format=log_format,
            handlers=handlers,
        )

        # Greeter message :)
        self.greeter_message()

        # Start logging message
        bias = " " * ((self.terminal_width // 2) - 13)
        print(f"{bias[:-1]}# # [ Start Logging ] # #\n")
        print("-" * self.terminal_width + "\n")

        # Log what we'll do next
        logging.info(
            f"{debug_prefix} We're done with the pre configuration of Python's behavior and loading prelude.toml configuration file"
        )

        # Log precise Python version
        sysversion = sys.version.replace("\n", " ").replace("  ", " ")
        logging.info(f"{debug_prefix} Running on Python: [{sysversion}]")

        # # The operating system we're on, one of "linux", "windows", "macos"

        # Get the desired name from a dict matching against os.name
        if platform is None:
            self.os = {
                "posix": "linux",
                "nt": "windows",
                "darwin": "macos"
            }.get(os.name)
        else:
            logging.info(
                f"{debug_prefix} Overriding platform OS to = [{platform}]")
            self.os = platform

        # Log which OS we're running
        logging.info(
            f"{debug_prefix} Running Modular Music Visualizer on Operating System: [{self.os}]"
        )

        # # Create interface's classes

        logging.info(f"{debug_prefix} Creating Utils() class")
        self.utils = Utils()

        logging.info(f"{debug_prefix} Creating Download() class")
        self.download = Download()

        # # Common directories between packages

        # Externals
        self.externals_dir = self.MMV_PACKAGE_ROOT / "externals"
        self.externals_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Externals dir is [{self.externals_dir}]")

        # Downloads (inside externals)
        self.downloads_dir = self.MMV_PACKAGE_ROOT / "externals" / "downloads"
        self.downloads_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Downloads dir is [{self.downloads_dir}]")

        # Assets dir
        self.assets_dir = self.MMV_PACKAGE_ROOT / "assets"
        self.assets_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Assets dir is [{self.assets_dir}]")

        # Data dir
        self.data_dir = self.MMV_PACKAGE_ROOT / "data"
        self.data_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Data dir is [{self.data_dir}]")

        # Shaders dir
        self.shaders_dir = self.MMV_PACKAGE_ROOT / "shaders"
        self.shaders_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Shaders dir is [{self.shaders_dir}]")

        # Screenshots dir
        self.screenshots_dir = self.MMV_PACKAGE_ROOT / "screenshots"
        self.screenshots_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Shaders dir is [{self.screenshots_dir}]")

        # Runtime dir
        self.runtime_dir = self.MMV_PACKAGE_ROOT / "runtime"
        logging.info(
            f"{debug_prefix} Runtime dir is [{self.runtime_dir}], deleting..")
        shutil.rmtree(self.runtime_dir, ignore_errors=True)
        self.runtime_dir.mkdir(parents=True, exist_ok=True)

        # Windoe juuuust in case
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Appending the Externals directory to system path juuuust in case..."
            )
            sys.path.append(self.externals_dir)

        # # Common files

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

        # # External dependencies where to append for PATH

        # Externals directory for Linux
        self.externals_dir_linux = self.MMV_PACKAGE_ROOT / "externals" / "linux"
        if self.os == "linux":
            logging.info(
                f"{debug_prefix} Externals directory for Linux OS is [{self.externals_dir_linux}]"
            )
            self.externals_dir_linux.mkdir(parents=True, exist_ok=True)

        # Externals directory for Windows
        self.externals_dir_windows = self.MMV_PACKAGE_ROOT / "externals" / "windows"
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Externals directory for Windows OS is [{self.externals_dir_windows}]"
            )
            self.externals_dir_windows.mkdir(parents=True, exist_ok=True)

        # Externals directory for macOS
        self.externals_dir_macos = self.MMV_PACKAGE_ROOT / "externals" / "macos"
        if self.os == "macos":
            logging.info(
                f"{debug_prefix} Externals directory for Darwin OS (macOS) is [{self.externals_dir_macos}]"
            )
            self.externals_dir_macos.mkdir(parents=True, exist_ok=True)

        # # This native platform externals dir
        self.externals_dir_this_platform = self.__get_platform_external_dir(
            self.os)
        logging.info(
            f"{debug_prefix} This platform externals directory is: [{self.externals_dir_this_platform}]"
        )

        # Update the externals search path (create one in this case)
        self.update_externals_search_path()

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)
    def __init__(self, top_level_interace, depth=LOG_NO_DEPTH, **kwargs):
        debug_prefix = "[MMVSkiaInterface.__init__]"
        ndepth = depth + LOG_NEXT_DEPTH
        self.top_level_interace = top_level_interace
        self.os = self.top_level_interace.os

        # Where this file is located, please refer using this on the whole package
        # Refer to it as self.mmv_main.mmvskia_interface.MMV_SKIA_ROOT at any depth in the code
        # This deals with the case we used pyinstaller and it'll get the executable path instead
        if getattr(sys, 'frozen', True):
            self.MMV_SKIA_ROOT = os.path.dirname(os.path.abspath(__file__))
            logging.info(
                f"{depth}{debug_prefix} Running directly from source code")
            logging.info(
                f"{depth}{debug_prefix} Modular Music Visualizer Python package [__init__.py] located at [{self.MMV_SKIA_ROOT}]"
            )
        else:
            self.MMV_SKIA_ROOT = os.path.dirname(
                os.path.abspath(sys.executable))
            logging.info(
                f"{depth}{debug_prefix} Running from release (sys.executable..?)"
            )
            logging.info(
                f"{depth}{debug_prefix} Modular Music Visualizer executable located at [{self.MMV_SKIA_ROOT}]"
            )

        # # Prelude configuration

        prelude_file = f"{self.MMV_SKIA_ROOT}{os.path.sep}mmv_skia_prelude.toml"
        logging.info(
            f"{depth}{debug_prefix} Attempting to load prelude file located at [{prelude_file}], we cannot continue if this is wrong.."
        )

        with open(prelude_file, "r") as f:
            self.prelude = toml.loads(f.read())

        # Log prelude configuration
        logging.info(
            f"{depth}{debug_prefix} Prelude configuration is: {self.prelude}")

        # # # Create MMV classes and stuff

        # Main class of MMV and tart MMV classes that main connects them, do not run
        self.mmv_main = MMVSkiaMain(interface=self)
        self.mmv_main.setup(depth=ndepth)

        # Utilities
        self.utils = Utils()

        # Configuring options
        self.audio_processing = AudioProcessingPresets(self)
        self.post_processing = self.mmv_main.canvas.configure

        # Log a separator to mark the end of the __init__ phase
        logging.info(f"{depth}{debug_prefix} Initialize phase done!")
        logging.info(LOG_SEPARATOR)

        self.configure_mmv_main()

        # Quit if code flow says so
        if self.prelude["flow"]["stop_at_interface_init"]:
            logging.critical(
                f"{ndepth}{debug_prefix} Not continuing because stop_at_interface_init key on prelude.toml is True"
            )
            sys.exit(0)
class MMVSkiaInterface:

    # This top level interface is the "global package manager" for every subproject / subimplementation of
    # MMV, it is the class that will deal with stuff not directly related to functionality of this class
    # and package here, mainly dealing with external deps, micro managing stuff, setting up logging,
    # loading "prelude" configurations, that is, defining behavior, not configs related to this package
    # and functionality.
    #
    # We create a MMV{Skia,Shader}Main class and we send this interface to it, and we send that instance
    # of MMV*Main to every other sub class so if we access self.mmv_main.mmvskia_interface we are accessing this
    # file here, MMVSkiaInterface, and we can quickly refer to the most top level package by doing
    # self.mmv_main.mmvskia_interface.top_level_interface, since this interface here is just the MMVSkia
    # interface for the mmvskia package while the top level one manages both MMVSkia and MMVShader
    #
    def __init__(self, top_level_interace, depth=LOG_NO_DEPTH, **kwargs):
        debug_prefix = "[MMVSkiaInterface.__init__]"
        ndepth = depth + LOG_NEXT_DEPTH
        self.top_level_interace = top_level_interace
        self.os = self.top_level_interace.os

        # Where this file is located, please refer using this on the whole package
        # Refer to it as self.mmv_main.mmvskia_interface.MMV_SKIA_ROOT at any depth in the code
        # This deals with the case we used pyinstaller and it'll get the executable path instead
        if getattr(sys, 'frozen', True):
            self.MMV_SKIA_ROOT = os.path.dirname(os.path.abspath(__file__))
            logging.info(
                f"{depth}{debug_prefix} Running directly from source code")
            logging.info(
                f"{depth}{debug_prefix} Modular Music Visualizer Python package [__init__.py] located at [{self.MMV_SKIA_ROOT}]"
            )
        else:
            self.MMV_SKIA_ROOT = os.path.dirname(
                os.path.abspath(sys.executable))
            logging.info(
                f"{depth}{debug_prefix} Running from release (sys.executable..?)"
            )
            logging.info(
                f"{depth}{debug_prefix} Modular Music Visualizer executable located at [{self.MMV_SKIA_ROOT}]"
            )

        # # Prelude configuration

        prelude_file = f"{self.MMV_SKIA_ROOT}{os.path.sep}mmv_skia_prelude.toml"
        logging.info(
            f"{depth}{debug_prefix} Attempting to load prelude file located at [{prelude_file}], we cannot continue if this is wrong.."
        )

        with open(prelude_file, "r") as f:
            self.prelude = toml.loads(f.read())

        # Log prelude configuration
        logging.info(
            f"{depth}{debug_prefix} Prelude configuration is: {self.prelude}")

        # # # Create MMV classes and stuff

        # Main class of MMV and tart MMV classes that main connects them, do not run
        self.mmv_main = MMVSkiaMain(interface=self)
        self.mmv_main.setup(depth=ndepth)

        # Utilities
        self.utils = Utils()

        # Configuring options
        self.audio_processing = AudioProcessingPresets(self)
        self.post_processing = self.mmv_main.canvas.configure

        # Log a separator to mark the end of the __init__ phase
        logging.info(f"{depth}{debug_prefix} Initialize phase done!")
        logging.info(LOG_SEPARATOR)

        self.configure_mmv_main()

        # Quit if code flow says so
        if self.prelude["flow"]["stop_at_interface_init"]:
            logging.critical(
                f"{ndepth}{debug_prefix} Not continuing because stop_at_interface_init key on prelude.toml is True"
            )
            sys.exit(0)

    # Read the function body for more info
    def configure_mmv_main(self, **kwargs):

        # Has the user chosen to watch the processing video realtime?
        self.mmv_main.context.audio_amplitude_multiplier = kwargs.get(
            "audio_amplitude_multiplier", 1)
        self.mmv_main.context.skia_render_backend = kwargs.get(
            "render_backend", "gpu")

        # # Encoding options

        # FFmpeg
        self.mmv_main.context.ffmpeg_pixel_format = kwargs.get(
            "ffmpeg_pixel_format", "auto")
        self.mmv_main.context.ffmpeg_dumb_player = kwargs.get(
            "ffmpeg_dumb_player", "auto")
        self.mmv_main.context.ffmpeg_hwaccel = kwargs.get(
            "ffmpeg_hwaccel", "auto")

        # x264 specific
        self.mmv_main.context.x264_use_opencl = kwargs.get(
            "x264_use_opencl", False)
        self.mmv_main.context.x264_preset = kwargs.get("x264_preset", "slow")
        self.mmv_main.context.x264_tune = kwargs.get("x264_tune", "film")
        self.mmv_main.context.x264_crf = kwargs.get("x264_crf", "17")

        # Pipe writer
        self.mmv_main.context.max_images_on_pipe_buffer = kwargs.get(
            "max_images_on_pipe_buffer", 20)

    # Execute MMV with the configurations we've done
    def run(self, depth=PACKAGE_DEPTH) -> None:
        debug_prefix = "[MMVSkiaInterface.run]"
        ndepth = depth + LOG_NEXT_DEPTH
        logging.info(LOG_SEPARATOR)

        # Log action
        logging.info(
            f"{depth}{debug_prefix} Configuration phase done, executing MMVSkiaMain.run().."
        )

        # Run configured mmv_main class
        self.mmv_main.run(depth=ndepth)

    # Define output video width, height and frames per second, defaults to 720p60
    def quality(self,
                width: int = 1280,
                height: int = 720,
                fps: int = 60,
                batch_size=2048,
                depth=PACKAGE_DEPTH) -> None:
        debug_prefix = "[MMVSkiaInterface.quality]"
        ndepth = depth + LOG_NEXT_DEPTH

        logging.info(
            f"{depth}{debug_prefix} Setting width={width} height={height} fps={fps} batch_size={batch_size}"
        )

        # Assign values
        self.mmv_main.context.width = width
        self.mmv_main.context.height = height
        self.mmv_main.context.fps = fps
        self.mmv_main.context.batch_size = batch_size
        self.width = width
        self.height = height
        self.resolution = [width, height]

        # Create or reset a mmv canvas with that target resolution
        logging.info(
            f"{depth}{debug_prefix} Creating / resetting canvas with that width and height"
        )
        self.mmv_main.canvas.create_canvas(depth=ndepth)
        logging.info(STEP_SEPARATOR)

    # Set the input audio file, raise exception if it does not exist
    def input_audio(self, path: str, depth=PACKAGE_DEPTH) -> None:
        debug_prefix = "[MMVSkiaInterface.input_audio]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action, do action
        logging.info(
            f"{depth}{debug_prefix} Set audio file path: [{path}], getting absolute path.."
        )
        self.mmv_main.context.input_audio_file = self.get_absolute_path(
            path, depth=ndepth)
        logging.info(STEP_SEPARATOR)

    # Set the input audio file, raise exception if it does not exist
    def input_midi(self, path: str, depth=PACKAGE_DEPTH) -> None:
        debug_prefix = "[MMVSkiaInterface.input_midi]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action, do action
        logging.info(
            f"{depth}{debug_prefix} Set MIDI file path: [{path}], getting absolute path.."
        )
        self.mmv_main.context.input_midi = self.get_absolute_path(path,
                                                                  depth=ndepth)
        logging.info(STEP_SEPARATOR)

    # Output path where we'll be saving the final video
    def output_video(self, path: str, depth=PACKAGE_DEPTH) -> None:
        debug_prefix = "[MMVSkiaInterface.output_video]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action, do action
        logging.info(
            f"{depth}{debug_prefix} Set output video path: [{path}], getting absolute path.."
        )
        self.mmv_main.context.output_video = self.utils.get_abspath(
            path, depth=ndepth)
        logging.info(STEP_SEPARATOR)

    # Offset where we cut the audio for processing, mainly for interpolation latency compensation
    def offset_audio_steps(self, steps: int = 0, depth=PACKAGE_DEPTH):
        debug_prefix = "[MMVSkiaInterface.offset_audio_steps]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action, do action
        logging.info(
            f"{depth}{debug_prefix} Offset audio in N steps: [{steps}]")
        self.mmv_main.context.offset_audio_before_in_many_steps = steps
        logging.info(STEP_SEPARATOR)

    # # [ MMV Objects ] # #

    # Add a given object to MMVSkiaAnimation content on a given layer
    def add(self, item, layer: int = 0, depth=PACKAGE_DEPTH) -> None:
        debug_prefix = "[MMVSkiaInterface.add]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Make layers until this given layer if they don't exist
        logging.info(
            f"{depth}{debug_prefix} Making animations layer until N = [{layer}]"
        )
        self.mmv_main.mmv_animation.mklayers_until(layer, depth=ndepth)

        # Check the type and add accordingly
        if self.utils.is_matching_type([item], [MMVSkiaImage]):
            logging.info(
                f"{depth}{debug_prefix} Add MMVSkiaImage object [{item}]")
            self.mmv_main.mmv_animation.content[layer].append(item)

        if self.utils.is_matching_type([item], [MMVSkiaGenerator]):
            logging.info(
                f"{depth}{debug_prefix} Add MMVSkiaGenerator object [{item}]")
            self.mmv_main.mmv_animation.generators.append(item)

        logging.info(STEP_SEPARATOR)

    # Get a blank MMVSkiaImage object with the first animation layer build up
    def image_object(self, depth=PACKAGE_DEPTH) -> MMVSkiaImage:
        debug_prefix = "[MMVSkiaInterface.image_object]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action
        logging.info(
            f"{depth}{debug_prefix} Creating blank MMVSkiaImage object and initializing first animation layer, returning it afterwards"
        )

        # Create blank MMVSkiaImage, init the animation layers for the user
        mmv_image_object = MMVSkiaImage(self.mmv_main, depth=ndepth)
        mmv_image_object.configure.init_animation_layer(depth=ndepth)

        # Return a pointer to the object
        logging.info(STEP_SEPARATOR)
        return mmv_image_object

    # Get a blank MMVSkiaGenerator object
    def generator_object(self, depth=PACKAGE_DEPTH):
        debug_prefix = "[MMVSkiaInterface.generator_object]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action
        logging.info(
            f"{depth}{debug_prefix} Creating blank MMVSkiaGenerator object, returning it afterwards"
        )

        # Create blank MMVSkiaGenerator, return a pointer to the object
        logging.info(STEP_SEPARATOR)
        return MMVSkiaGenerator(self.mmv_main, depth=ndepth)

    # # [ Utilities ] # #

    # Random file from a given path directory (loading random backgrounds etc)
    def random_file_from_dir(self, path, depth=PACKAGE_DEPTH):
        debug_prefix = "[MMVSkiaInterface.random_file_from_dir]"
        ndepth = depth + LOG_NEXT_DEPTH

        logging.info(
            f"{depth}{debug_prefix} Get absolute path and returning random file from directory: [{path}]"
        )

        logging.info(STEP_SEPARATOR)
        return self.utils.random_file_from_dir(self.utils.get_abspath(
            path, depth=ndepth),
                                               depth=ndepth)

    # Make the directory if it doesn't exist
    def make_directory_if_doesnt_exist(self,
                                       path: str,
                                       depth=PACKAGE_DEPTH,
                                       silent=True) -> None:
        debug_prefix = "[MMVSkiaInterface.make_directory_if_doesnt_exist]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action
        logging.info(
            f"{depth}{debug_prefix} Make directory if doesn't exist [{path}], get absolute realpath and mkdir_dne"
        )

        # Get absolute and realpath, make directory if doens't exist (do the action)
        path = self.utils.get_abspath(path, depth=ndepth, silent=silent)
        self.utils.mkdir_dne(path, depth=ndepth)
        logging.info(STEP_SEPARATOR)

    # Make the directory if it doesn't exist
    def delete_directory(self,
                         path: str,
                         depth=PACKAGE_DEPTH,
                         silent=False) -> None:
        debug_prefix = "[MMVSkiaInterface.delete_directory]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action
        logging.info(
            f"{depth}{debug_prefix} Delete directory [{path}], get absolute realpath and rmdir"
        )

        # Get absolute and realpath, delete directory (do the action)
        path = self.utils.get_abspath(path, depth=ndepth, silent=silent)
        self.utils.rmdir(path, depth=ndepth)
        logging.info(STEP_SEPARATOR)

    # Get the absolute path to a file or directory, absolute starts with / on *nix and LETTER:// on Windows
    # we expect it to exist so we quit if don't since this is the interface class?
    def get_absolute_path(self, path, message="path", depth=PACKAGE_DEPTH):
        debug_prefix = "[MMVSkiaInterface.get_absolute_path]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action
        logging.info(
            f"{depth}{debug_prefix} Getting absolute path of [{path}], also checking its existence"
        )

        # Get the absolute path
        path = self.utils.get_abspath(path, depth=ndepth)

        if not os.path.exists(path):
            raise FileNotFoundError(f"Input {message} does not exist {path}")
        logging.info(STEP_SEPARATOR)
        return path

    # If we ever need any unique id..?
    def get_unique_id(self):
        return self.utils.get_unique_id()

    # # [ Experiments / sub projects ] # #

    # Get a pygradienter object with many workers for rendering
    def pygradienter(self, depth=PACKAGE_DEPTH, **kwargs):
        debug_prefix = "[MMVSkiaInterface.pygradienter]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action
        logging.info(
            f"{depth}{debug_prefix} Generating and returning one PyGradienter object"
        )

        logging.info(STEP_SEPARATOR)
        return PyGradienter(self.mmv_main, depth=ndepth, **kwargs)

    # Returns a cmn_midi.py MidiFile class
    def get_midi_class(self):
        return MidiFile()

    # # [ Advanced ] # #

    def advanced_audio_processing_constants(self,
                                            where_decay_less_than_one,
                                            value_at_zero,
                                            depth=PACKAGE_DEPTH):
        debug_prefix = "[MMVSkiaInterface.advanced_audio_processing_constants]"
        ndepth = depth + LOG_NEXT_DEPTH

        # Log action
        logging.info(
            f"{depth}{debug_prefix} Setting AudioProcessing constants to where_decay_less_than_one=[{where_decay_less_than_one}], value_at_zero=[{value_at_zero}]"
        )

        self.mmv_main.audio.where_decay_less_than_one = where_decay_less_than_one
        self.mmv_main.audio.value_at_zero = value_at_zero
示例#23
0
class MMVPackageInterface:

    # Hello world!
    def greeter_message(self) -> None:
        debug_prefix = "[MMVPackageInterface.greeter_message]"

        # Get a bias for printing the message centered
        self.terminal_width = shutil.get_terminal_size()[0]
        bias = " " * (math.floor(self.terminal_width / 2) - 14)

        message = \
f"""{debug_prefix} Show greeter message\n{"-"*self.terminal_width}
{bias} __  __   __  __  __     __
{bias}|  \\/  | |  \\/  | \\ \\   / /
{bias}| |\\/| | | |\\/| |  \\ \\ / / 
{bias}| |  | | | |  | |   \\ V /  
{bias}|_|  |_| |_|  |_|    \\_/   
{bias}
{bias} Modular Music Visualizer                      
{(2 + int( (self.terminal_width/2) - (len("Version") + len(self.version)/2) ))*" "}Version {self.version}
{"-"*self.terminal_width}
"""
        logging.info(message)

    # Thanks message with some official links, warnings
    def thanks_message(self):
        debug_prefix = "[MMVPackageInterface.thanks_message]"

        # Get a bias for printing the message centered
        self.terminal_width = shutil.get_terminal_size()[0]
        bias = " " * (math.floor(self.terminal_width / 2) - 45)

        message = \
f"""{debug_prefix} Show thanks message
\n{"-"*self.terminal_width}\n
{bias}[+-------------------------------------------------------------------------------------------+]
{bias} |                                                                                           |
{bias} |              :: Thanks for using the Modular Music Visualizer project !! ::               |
{bias} |              ==============================================================               |
{bias} |                                                                                           |
{bias} |  Here's a few official links for MMV:                                                     |
{bias} |                                                                                           |
{bias} |    - Telegram group:          [          https://t.me/modular_music_visualizer         ]  |
{bias} |    - GitHub Repository:       [ https://github.com/Tremeschin/modular-music-visualizer ]  |
{bias} |    - GitLab Repository:       [ https://gitlab.com/Tremeschin/modular-music-visualizer ]  |
{bias} |                                                                                           |
{bias} |  > Always check for the copyright info on the material you are using (audios, images)     |
{bias} |  before distributing the content generated with MMV, I take absolutely no responsibility  |
{bias} |  for any UGC (user generated content) violations. See LICENSE file as well.               |
{bias} |                                                                                           |
{bias} |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  |
{bias} |                                                                                           |
{bias} |             Don't forget sharing your releases made with MMV on the discussion groups :)  |
{bias} |                 Feel free asking for help or giving new ideas for the project as well !!  |
{bias} |                                                                                           |
{bias}[+-------------------------------------------------------------------------------------------+]
\n{"-"*self.terminal_width}
"""
        logging.info(message)

    # MMVShader for some post processing or visualization generation
    def ___printshadersmode(self):
        if not hasattr(self, "___printshader"):
            self.___printshader = None
            self.terminal_width = shutil.get_terminal_size()[0]
            bias = " " * (math.floor(self.terminal_width / 2) - 19)
            message = \
f"""Show extension\n{"="*self.terminal_width}
{bias} _____ _               _               
{bias}/  ___| |             | |              
{bias}\\ `--.| |__   __ _  __| | ___ _ __ ___ 
{bias} `--. \\ '_ \\ / _` |/ _` |/ _ \\ '__/ __|
{bias}/\\__/ / | | | (_| | (_| |  __/ |  \\__ \\
{bias}\\____/|_| |_|\\__,_|\\__,_|\\___|_|  |___/
{bias}                            
{bias}
{bias}             + MMV Mode +
{"="*self.terminal_width}
"""
            logging.info(message)

    # Get a moderngl wrapper / interface for rendering fragment shaders, getting their
    # contents, map images, videos and even other shaders into textures
    def get_mmv_shader_mgl(self, **kwargs):
        self.___printshadersmode()
        from mmv.mmvshader.mmv_shader_mgl import MMVShaderMGL
        return MMVShaderMGL

    # Return shader maker interface
    def get_mmv_shader_maker(self, **kwargs):
        self.___printshadersmode()
        from mmv.mmvshader.mmv_shader_maker import MMVShaderMaker
        return MMVShaderMaker

    # Return one (usually required) setting up encoder unless using preview window
    def get_ffmpeg_wrapper(self):
        debug_prefix = "[MMVPackageInterface.get_ffmpeg_wrapper]"
        from mmv.common.wrappers.wrap_ffmpeg import FFmpegWrapper
        logging.info(f"{debug_prefix} Return FFmpegWrapper")
        return FFmpegWrapper(ffmpeg_binary_path=self.find_binary("ffmpeg"))

    # Return FFplay wrapper, rarely needed but just in case
    def get_ffplay_wrapper(self):
        debug_prefix = "[MMVPackageInterface.get_ffplay_wrapper]"
        from mmv.common.wrappers.wrap_ffplay import FFplayWrapper
        logging.info(f"{debug_prefix} Return FFplayWrapper")
        return FFplayWrapper()

    # # Audio sources

    # Real time, reads from a loopback device
    def get_audio_source_realtime(self):
        debug_prefix = "[MMVPackageInterface.get_audio_source_realtime]"
        from mmv.common.cmn_audio import AudioSourceRealtime
        return AudioSourceRealtime()

    # File source, used for headless rendering
    def get_audio_source_file(self):
        debug_prefix = "[MMVPackageInterface.get_audio_source_file]"
        from mmv.common.cmn_audio import AudioSourceFile
        return AudioSourceFile(ffmpeg_wrapper=self.get_ffmpeg_wrapper())

    # Real time, reads from a loopback device
    def get_jumpcutter(self):
        debug_prefix = "[MMVPackageInterface.get_jumpcutter]"
        self.terminal_width = shutil.get_terminal_size()[0]
        bias = " " * (math.floor(self.terminal_width / 2) - 28)
        message = \
f"""{debug_prefix} Show extension\n{"="*self.terminal_width}
{bias}   ___                       _____       _   _            
{bias}  |_  |                     /  __ \\     | | | |           
{bias}    | |_   _ _ __ ___  _ __ | /  \\/_   _| |_| |_ ___ _ __ 
{bias}    | | | | | '_ ` _ \\| '_ \\| |   | | | | __| __/ _ \\ '__|
{bias}/\\__/ / |_| | | | | | | |_) | \\__/\\ |_| | |_| ||  __/ |   
{bias}\\____/ \\__,_|_| |_| |_| .__/ \\____/\\__,_|\\__|\\__\\___|_|   
{bias}                      | |                                 
{bias}                      |_|                                
{bias}
{bias}                   + MMV Extension +
{"="*self.terminal_width}
"""
        logging.info(message)
        from mmv.extra.extra_jumpcutter import JumpCutter
        return JumpCutter(ffmpeg_wrapper=self.get_ffmpeg_wrapper())

    # Main interface class, mainly sets up root dirs, get config, distributes classes
    # Send platform = "windows", "macos", "linux" for forcing a specific one
    def __init__(self, platform=None, **kwargs) -> None:
        debug_prefix = "[MMVPackageInterface.__init__]"

        self.version = "3.2: rolling"

        # Where this file is located, please refer using this on the whole package
        # Refer to it as self.mmv_skia_main.MMV_PACKAGE_ROOT at any depth in the code
        # This deals with the case we used pyinstaller and it'll get the executable path instead
        if getattr(sys, 'frozen', True):
            self.MMV_PACKAGE_ROOT = Path(
                os.path.dirname(os.path.abspath(__file__)))
            print(f"{debug_prefix} Running directly from source code")
        else:
            self.MMV_PACKAGE_ROOT = Path(
                os.path.dirname(os.path.abspath(sys.executable)))
            print(f"{debug_prefix} Running from release (sys.executable..?)")

        print(
            f"{debug_prefix} Modular Music Visualizer Python package [__init__.py] or executable located at [{self.MMV_PACKAGE_ROOT}]"
        )

        # # Load prelude configuration

        print(f"{debug_prefix} Loading prelude configuration file")

        # Build the path the prelude file should be located at
        prelude_file = self.MMV_PACKAGE_ROOT / "prelude.toml"
        print(
            f"{debug_prefix} Attempting to load prelude file located at [{prelude_file}], we cannot continue if this is wrong.."
        )

        # Load the prelude file
        with open(prelude_file, "r") as f:
            self.prelude = toml.loads(f.read())

        print(
            f"{debug_prefix} Loaded prelude configuration file, data: [{self.prelude}]"
        )

        # # # Logging

        # # We can now set up logging as we have where this file is located at

        # # Reset current handlers if any
        print(
            f"{debug_prefix} Resetting Python's logging logger handlers to empty list"
        )

        # Get logger and empty the list
        logger = logging.getLogger()
        logger.handlers = []

        # Handlers on logging to file and shell output, the first one if the user says to
        handlers = [logging.StreamHandler(sys.stdout)]

        # Loglevel is defined in the prelude.toml configuration
        LOG_LEVEL = {
            "critical": logging.CRITICAL,
            "debug": logging.DEBUG,
            "error": logging.ERROR,
            "info": logging.INFO,
            "warn": logging.warning,
            "notset": logging.NOTSET,
        }.get(self.prelude["logging"]["log_level"])

        # If user chose to log to a file, add its handler..
        if self.prelude["logging"]["log_to_file"]:

            # Hard coded where the log file will be located
            # this is only valid for the last time we run this software
            self.LOG_FILE = self.MMV_PACKAGE_ROOT / "last_log.log"

            # Reset the log file
            with open(self.LOG_FILE, "w") as f:
                print(
                    f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]"
                )
                f.write("")

            # Verbose and append the file handler
            print(
                f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]")
            handlers.append(
                logging.FileHandler(filename=self.LOG_FILE, encoding='utf-8'))

        # .. otherwise just keep the StreamHandler to stdout

        log_format = {
            "informational":
            "[%(levelname)-8s] [%(filename)-32s:%(lineno)-3d] (%(relativeCreated)-6d) %(message)s",
            "pretty":
            "[%(levelname)-8s] (%(relativeCreated)-5d)ms %(message)s",
            "economic":
            "[%(levelname)s::%(filename)s::%(lineno)d] %(message)s",
            "onlymessage": "%(message)s"
        }.get(self.prelude["logging"]["log_format"])

        # Start the logging global class, output to file and stdout
        logging.basicConfig(
            level=LOG_LEVEL,
            format=log_format,
            handlers=handlers,
        )

        # Greeter message :)
        self.greeter_message()

        # Start logging message
        bias = " " * ((self.terminal_width // 2) - 13)
        print(f"{bias[:-1]}# # [ Start Logging ] # #\n")
        print("-" * self.terminal_width + "\n")

        # Log what we'll do next
        logging.info(
            f"{debug_prefix} We're done with the pre configuration of Python's behavior and loading prelude.toml configuration file"
        )

        # Log precise Python version
        sysversion = sys.version.replace("\n", " ").replace("  ", " ")
        logging.info(f"{debug_prefix} Running on Python: [{sysversion}]")

        # # The operating system we're on, one of "linux", "windows", "macos"

        # Get the desired name from a dict matching against os.name
        if platform is None:
            self.os = {
                "posix": "linux",
                "nt": "windows",
                "darwin": "macos"
            }.get(os.name)
        else:
            logging.info(
                f"{debug_prefix} Overriding platform OS to = [{platform}]")
            self.os = platform

        # Log which OS we're running
        logging.info(
            f"{debug_prefix} Running Modular Music Visualizer on Operating System: [{self.os}]"
        )

        # # Create interface's classes

        logging.info(f"{debug_prefix} Creating Utils() class")
        self.utils = Utils()

        logging.info(f"{debug_prefix} Creating Download() class")
        self.download = Download()

        # # Common directories between packages

        # Externals
        self.externals_dir = self.MMV_PACKAGE_ROOT / "externals"
        self.externals_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Externals dir is [{self.externals_dir}]")

        # Downloads (inside externals)
        self.downloads_dir = self.MMV_PACKAGE_ROOT / "externals" / "downloads"
        self.downloads_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Downloads dir is [{self.downloads_dir}]")

        # Assets dir
        self.assets_dir = self.MMV_PACKAGE_ROOT / "assets"
        self.assets_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Assets dir is [{self.assets_dir}]")

        # Data dir
        self.data_dir = self.MMV_PACKAGE_ROOT / "data"
        self.data_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Data dir is [{self.data_dir}]")

        # Shaders dir
        self.shaders_dir = self.MMV_PACKAGE_ROOT / "shaders"
        self.shaders_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Shaders dir is [{self.shaders_dir}]")

        # Screenshots dir
        self.screenshots_dir = self.MMV_PACKAGE_ROOT / "screenshots"
        self.screenshots_dir.mkdir(parents=True, exist_ok=True)
        logging.info(f"{debug_prefix} Shaders dir is [{self.screenshots_dir}]")

        # Runtime dir
        self.runtime_dir = self.MMV_PACKAGE_ROOT / "runtime"
        logging.info(
            f"{debug_prefix} Runtime dir is [{self.runtime_dir}], deleting..")
        shutil.rmtree(self.runtime_dir, ignore_errors=True)
        self.runtime_dir.mkdir(parents=True, exist_ok=True)

        # Windoe juuuust in case
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Appending the Externals directory to system path juuuust in case..."
            )
            sys.path.append(self.externals_dir)

        # # Common files

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

        # # External dependencies where to append for PATH

        # Externals directory for Linux
        self.externals_dir_linux = self.MMV_PACKAGE_ROOT / "externals" / "linux"
        if self.os == "linux":
            logging.info(
                f"{debug_prefix} Externals directory for Linux OS is [{self.externals_dir_linux}]"
            )
            self.externals_dir_linux.mkdir(parents=True, exist_ok=True)

        # Externals directory for Windows
        self.externals_dir_windows = self.MMV_PACKAGE_ROOT / "externals" / "windows"
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Externals directory for Windows OS is [{self.externals_dir_windows}]"
            )
            self.externals_dir_windows.mkdir(parents=True, exist_ok=True)

        # Externals directory for macOS
        self.externals_dir_macos = self.MMV_PACKAGE_ROOT / "externals" / "macos"
        if self.os == "macos":
            logging.info(
                f"{debug_prefix} Externals directory for Darwin OS (macOS) is [{self.externals_dir_macos}]"
            )
            self.externals_dir_macos.mkdir(parents=True, exist_ok=True)

        # # This native platform externals dir
        self.externals_dir_this_platform = self.__get_platform_external_dir(
            self.os)
        logging.info(
            f"{debug_prefix} This platform externals directory is: [{self.externals_dir_this_platform}]"
        )

        # Update the externals search path (create one in this case)
        self.update_externals_search_path()

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

    # Get the target externals dir for this platform
    def __get_platform_external_dir(self, platform):
        debug_prefix = "[MMVPackageInterface.__get_platform_external_dir]"

        # # This platform externals dir
        externals_dir = {
            "linux": self.externals_dir_linux,
            "windows": self.externals_dir_windows,
            "macos": self.externals_dir_macos,
        }.get(platform)

        # mkdir dne just in case cause we asked for this?
        externals_dir.mkdir(parents=True, exist_ok=True)

        # log action
        logging.info(
            f"{debug_prefix} Return external dir for platform [{platform}] -> [{externals_dir}]"
        )
        return externals_dir

    # Update the self.EXTERNALS_SEARCH_PATH to every recursive subdirectory on the platform's externals dir
    def update_externals_search_path(self):
        debug_prefix = "[MMVPackageInterface.update_externals_search_path]"

        # The subdirectories on this platform externals folder
        externals_subdirs = self.utils.get_recursively_all_subdirectories(
            self.externals_dir_this_platform)

        # When using some function like Utils.get_executable_with_name, it have an argument
        # called extra_paths, add this for searching for the full externals directory.
        # Preferably use this interface methods like find_binary instead
        self.EXTERNALS_SEARCH_PATH = [self.externals_dir_this_platform]

        # If we do have subdirectories on this platform externals then append to it
        if externals_subdirs:
            self.EXTERNALS_SEARCH_PATH += externals_subdirs

    # Search for something in system's PATH, also searches for the externals folder
    # Don't append the extra .exe because Linux, macOS doesn't have these, returns False if no binary was found
    def find_binary(self, binary):
        debug_prefix = "[MMVPackageInterface.find_binary]"

        # Append .exe for Windows
        if (self.os == "windows") and (not binary.endswith(".exe")):
            binary += ".exe"

        # Log action
        logging.info(
            f"{debug_prefix} Finding binary in PATH and EXTERNALS directories: [{binary}]"
        )
        return self.utils.get_executable_with_name(
            binary, extra_paths=self.EXTERNALS_SEARCH_PATH)

    # Make sure we have some target Externals, downloads latest release for them.
    # For forcing to download the Windows binaries for a release, send platform="windows" for overwriting
    # otherwise it'll be set to this class's os.
    #
    # For FFmpeg, mpv: Linux and macOS people please install from your distro's package manager.
    #
    # Possible values for target are: ["ffmpeg", "mpv", "musescore"]
    #
    def check_download_externals(self, target_externals=[], platform=None):
        debug_prefix = "[MMVPackageInterface.check_download_externals]"

        # Overwrite os if user set to a specific one
        if platform is None:
            platform = self.os
        else:
            # Error assertion, only allow linux, macos or windows target os
            valid = ["linux", "macos", "windows"]
            if not platform in valid:
                err = f"Target os [{platform}] not valid: should be one of {valid}"
                logging.error(f"{debug_prefix} {err}")
                raise RuntimeError(err)

        # Force the externals argument to be a list
        target_externals = self.utils.force_list(target_externals)

        # Log action
        logging.info(
            f"{debug_prefix} Checking externals {target_externals} for os = [{platform}]"
        )

        # We're frozen (running from release..)
        if getattr(sys, 'frozen', False):
            logging.info(
                f"{debug_prefix} Not checking for externals because is executable build.. (should have them bundled?)"
            )
            return

        # Short hand
        sep = os.path.sep

        # The target externals dir for this platform, it must be windows if we're here..
        target_externals_dir = self.__get_platform_external_dir(platform)

        # For each target external
        for external in target_externals:
            debug_prefix = "[MMVPackageInterface.check_download_externals]"
            logging.info(
                f"{debug_prefix} Checking / downloading external: [{external}] for platform [{platform}]"
            )

            # # FFmpeg / FFprobe

            if external == "ffmpeg":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(binary="ffmpeg")
                    continue

                # If we don't have FFmpeg binary on externals dir
                if not self.find_binary("ffmpeg.exe"):

                    # Get the latest release number of ffmpeg
                    repo = "https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest"
                    logging.info(
                        f"{debug_prefix} Getting latest release info on repository: [{repo}]"
                    )
                    ffmpeg_release = json.loads(
                        self.download.get_html_content(repo))

                    # The assets (downloadable stuff)
                    assets = ffmpeg_release["assets"]
                    logging.info(
                        f"{debug_prefix} Available assets to download (checking for non shared, gpl, non vulkan release):"
                    )

                    # Parsing the version we target and want
                    for item in assets:

                        # The name of the
                        name = item["name"]
                        logging.info(f"{debug_prefix} - [{name}]")

                        # Expected stuff
                        is_lgpl = "lgpl" in name
                        is_shared = "shared" in name
                        have_vulkan = "vulkan" in name
                        from_master = "N" in name

                        # Log what we expect
                        logging.info(
                            f"{debug_prefix} - :: Is LGPL:                   [{is_lgpl:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Is Shared:                 [{is_shared:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Have Vulkan:               [{have_vulkan:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Master branch (N in name): [{from_master:<1}] (expect: 0)"
                        )

                        # We have a match!
                        if not (is_lgpl + is_shared + have_vulkan +
                                from_master):
                            logging.info(
                                f"{debug_prefix} - >> :: We have a match!!")
                            download_url = item["browser_download_url"]
                            break

                    # Where we'll download from
                    logging.info(
                        f"{debug_prefix} Download URL: [{download_url}]")

                    # Where we'll save the compressed zip of FFmpeg
                    ffmpeg_zip = self.downloads_dir + f"{sep}{name}"

                    # Download FFmpeg build
                    self.download.wget(download_url, ffmpeg_zip,
                                       f"FFmpeg v={name}")

                    # Extract the files
                    self.download.extract_zip(ffmpeg_zip, target_externals_dir)

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [ffmpeg] binary in externals / system path!!"
                    )

            # # MPV FIXME: deprecate future version

            if external == "mpv":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(
                        binary="mpv",
                        help_fix=f"Visit [https://mpv.io/installation/]")
                    continue

                # If we don't have mpv binary on externals dir or system's path
                if not self.find_binary("mpv"):

                    mpv_7z_version = "mpv-x86_64-20201220-git-dde0189.7z"

                    # Where we'll save the compressed zip of FFmpeg
                    mpv_7z = self.downloads_dir + f"{sep}{mpv_7z_version}"

                    # Download mpv build
                    self.download.wget(
                        f"https://sourceforge.net/projects/mpv-player-windows/files/64bit/{mpv_7z_version}/download",
                        mpv_7z, f"MPV v=20201220-git-dde0189")

                    # Where to extract final mpv
                    mpv_extracted_folder = f"{self.externals_dir_this_platform}{sep}" + mpv_7z_version.replace(
                        ".7z", "")
                    self.utils.mkdir_dne(path=mpv_extracted_folder)

                    # Extract the files
                    self.download.extract_file(mpv_7z, mpv_extracted_folder)

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [mpv] binary in externals / system path!!"
                    )

            # # Musescore

            if external == "musescore":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(
                        binary="musescore",
                        help_fix=
                        f"Go to [https://musescore.org/en/download] and install for your platform"
                    )
                    continue

                # If we don't have musescore binary on externals dir or system's path
                if not self.find_binary("musescore"):

                    # Version we want
                    musescore_version = "v3.5.2/MuseScorePortable-3.5.2.311459983-x86.paf.exe"

                    # Download musescore
                    self.download.wget(
                        f"https://cdn.jsdelivr.net/musescore/{musescore_version}",
                        f"{self.externals_dir_this_platform}{sep}musescore.exe",
                        f"Musescore Portable v=[{musescore_version}]")

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [musescore] binary in externals / system path!!"
                    )

            # Update the externals search path because we downloaded stuff
            self.update_externals_search_path()

    # Ensure we have an external dependency we can't micro manage because too much entropy
    def __cant_micro_manage_external_for_you(self, binary, help_fix=None):
        debug_prefix = "[MMVPackageInterface.__cant_micro_manage_external_for_you]"

        logging.warning(
            f"{debug_prefix} You are using Linux or macOS, please make sure you have [{binary}] package binary installed on your distro or on homebrew, we'll just check for it now, can't continue if you don't have it.."
        )

        # Can't continue
        if not self.find_binary(binary):
            logging.error(
                f"{debug_prefix} Couldn't find lowercase [{binary}] binary on PATH, install from your Linux distro package manager / macOS homebrew, please install it"
            )

            # Log any extra help we give the user
            if help_fix is not None:
                logging.error(f"{debug_prefix} {help_fix}")

            # Exit with non zero error code
            sys.exit(-1)
class PyGradienterProcessing():
    def __init__(self, profile, width, height, quiet=False):

        # Load the profile and a config
        self.profile = profile(width, height)
        self.width = width
        self.height = height
        self.quiet = quiet

        # Create classes
        self.utils = Utils()

        # Empty / "static" variables
        # self.unique_string = ""
        self.nodes = []
        self.ROOT = self.utils.get_root()

        # Create a empty canvas
        self.new_canvas()

    # Create a black canvas as a list and starting image
    def new_canvas(self):
        self.canvas = np.zeros([self.height, self.width, 4], dtype=np.uint8)

        # Set alpha channel to 255
        for i, _ in enumerate(self.canvas):
            for j, _ in enumerate(self.canvas[i]):
                self.canvas[i][j][3] = 255

    # Replace "width" with self.width and "height" with self.height on the setting
    def pos_replace(self, s):
        return str(s).replace("width",
                              str(self.width)).replace("height",
                                                       str(self.height))

    # Main routine on making the images
    def generate(self, image_id):

        random.seed(uuid.uuid4())

        # Add profile nodes
        for node in self.profile.generate_nodes():
            self.nodes.append(node)

        if not self.quiet:
            print("Generating image id [%s]" % image_id)

        # Loop through the image X and Y pixels
        for y in range(self.height):
            for x in range(self.width):

                # The sum of the distances
                distances = np.zeros(len(self.nodes))

                # The actual pixel we'll be setting the color to
                this_pixel = np.array([0, 0, 0])

                # For each node, calculate its distance
                for i, node in enumerate(self.nodes):

                    # Calculate the raw distance between two nodes
                    distance = self.profile.calculate_distance_between_nodes(
                        [x, y], node.la)

                    # Add to the total sum
                    distances[i] = distance

                    # If there is only one node or the distance is zero to a note, set it to the node color
                    if distances[-1] == 0:
                        this_pixel = list(node.color)

                # Loop through the colors

                # If the pixel is not inside a node
                if not 0 in distances:
                    this_pixel = self.profile.get_pixel_by_distances_and_nodes(
                        distances, self.nodes)

                # Generate (hopefully) a unique string to save the images
                # self.unique_string += str(distances[-1] * this_pixel[0] * this_pixel[1] * this_pixel[2] * time.time())[0:1]

                # Change and activate the pixel colors by their value
                self.canvas[y][x] = self.profile.pixel_color_transformations(
                    this_pixel, x, y, distances)

        if not self.quiet:
            print("Finished generating image id [%s]" % image_id)

    # Save an image to disk
    def save(self, path):

        if not self.config["quiet"]:
            print("Save id [%s]" % self.id)

        # Get a image from the numpy array, smooth it a bit and save
        img = Image.fromarray(self.canvas, mode="RGBA")
        img = img.filter(ImageFilter.SMOOTH)
        img.save(path, quality=95)
示例#25
0
class MMVPackageInterface:

    # Hello world!
    def greeter_message(self) -> None:
        debug_prefix = "[MMVPackageInterface.greeter_message]"

        self.terminal_width = shutil.get_terminal_size()[0]

        bias = " " * (math.floor(self.terminal_width / 2) - 14)

        message = \
f"""{debug_prefix} Show greeter message\n{"-"*self.terminal_width}
{bias} __  __   __  __  __     __
{bias}|  \\/  | |  \\/  | \\ \\   / /
{bias}| |\\/| | | |\\/| |  \\ \\ / / 
{bias}| |  | | | |  | |   \\ V /  
{bias}|_|  |_| |_|  |_|    \\_/   
{bias}
{bias} Modular Music Visualizer                      
{bias[:-1]}{(21-len("Version")-len(self.version))*" "}Version {self.version}
{"-"*self.terminal_width}
"""
        logging.info(message)

    def thanks_message(self):
        debug_prefix = "[MMVPackageInterface.thanks_message]"

        # # Print thanks message :)

        self.terminal_width = shutil.get_terminal_size()[0]

        bias = " " * (math.floor(self.terminal_width / 2) - 45)
        message = \
f"""{debug_prefix} Show thanks message
\n{"-"*self.terminal_width}\n
{bias}[+-------------------------------------------------------------------------------------------+]
{bias} |                                                                                           |
{bias} |              :: Thanks for using the Modular Music Visualizer project !! ::               |
{bias} |              ==============================================================               |
{bias} |                                                                                           |
{bias} |  Here's a few official links for MMV:                                                     |
{bias} |                                                                                           |
{bias} |    - Telegram group:          [          https://t.me/modular_music_visualizer         ]  |
{bias} |    - GitHub Repository:       [ https://github.com/Tremeschin/modular-music-visualizer ]  |
{bias} |    - GitLab Repository:       [ https://gitlab.com/Tremeschin/modular-music-visualizer ]  |
{bias} |                                                                                           |
{bias} |  > Always check for the copyright info on the material you are using (audios, images)     |
{bias} |  before distributing the content generated with MMV, I take absolutely no responsibility  |
{bias} |  for any UGC (user generated content) violations. See LICENSE file as well.               |
{bias} |                                                                                           |
{bias} |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  |
{bias} |                                                                                           |
{bias} |             Don't forget sharing your releases made with MMV on the discussion groups :)  |
{bias} |                 Feel free asking for help or giving new ideas for the project as well !!  |
{bias} |                                                                                           |
{bias}[+-------------------------------------------------------------------------------------------+]
\n{"-"*self.terminal_width}
"""
        logging.info(message)

    # MMVSkia works with glfw plus Skia to draw on a GL canvas and pipe
    # through FFmpeg to render a final video. Have Piano Roll options
    # and modules as well!!
    def get_skia_interface(self, **kwargs):
        debug_prefix = "[MMVPackageInterface.get_skia_interface]"

        from mmv.mmvskia import MMVSkiaInterface

        logging.info(
            f"{debug_prefix} Get and return MMVSkiaInterface, kwargs: {kwargs}"
        )

        return MMVSkiaInterface(top_level_interace=self, **kwargs)

    # MMVShader works with GLSL shaders through MPV. Currently most
    # applicable concept is post processing which bumps MMV quality
    # by a lot
    def get_shader_interface(self):
        debug_prefix = "[MMVPackageInterface.get_shader_interface]"
        from mmv.mmvshader import MMVShaderInterface
        logging.info(f"{debug_prefix} Return MMVShaderInterface")
        return MMVShaderInterface(top_level_interace=self)

    # Return one (usually required) setting up encoder
    def get_ffmpeg_wrapper(self):
        debug_prefix = "[MMVPackageInterface.get_ffmpeg_wrapper]"
        from mmv.common.wrappers.wrap_ffmpeg import FFmpegWrapper
        logging.info(f"{debug_prefix} Return FFmpegWrapper")
        return FFmpegWrapper()

    # Return FFplay wrapper, rarely needed but just in case
    def get_ffplay_wrapper(self):
        debug_prefix = "[MMVPackageInterface.get_ffplay_wrapper]"
        from mmv.common.wrappers.wrap_ffplay import FFplayWrapper
        logging.info(f"{debug_prefix} Return FFplayWrapper")
        return FFplayWrapper()

    # Main interface class, mainly sets up root dirs, get config, distributes classes
    # Send platform = "windows", "macos", "linux" for forcing a specific one
    def __init__(self, platform=None, **kwargs) -> None:
        debug_prefix = "[MMVPackageInterface.__init__]"

        # Versioning
        self.version = "2.6"

        # Can only run on Python 64 bits, this expression returns 32 if 32 bit installation
        # and 64 if 64 bit installation, we assert that (assume it's true, quit if it isn't)
        assert (struct.calcsize("P") * 8) == 64, (
            "You don't have an 64 bit Python installation, MMV will not work on 32 bit Python "
            "because skia-python package only distributes 64 bit Python wheels (bundles).\n"
            "This is out of my control, Skia devs don't release 32 bit version of Skia anyways\n\n"
            "See issue [https://github.com/kyamagu/skia-python/issues/21]")

        # # Get this file's path

        sep = os.path.sep

        # Where this file is located, please refer using this on the whole package
        # Refer to it as self.mmv_skia_main.MMV_PACKAGE_ROOT at any depth in the code
        # This deals with the case we used pyinstaller and it'll get the executable path instead
        if getattr(sys, 'frozen', True):
            self.MMV_PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__))
            print(f"{debug_prefix} Running directly from source code")
            print(
                f"{debug_prefix} Modular Music Visualizer Python package [__init__.py] located at [{self.MMV_PACKAGE_ROOT}]"
            )
        else:
            self.MMV_PACKAGE_ROOT = os.path.dirname(
                os.path.abspath(sys.executable))
            print(f"{debug_prefix} Running from release (sys.executable..?)")
            print(
                f"{debug_prefix} Modular Music Visualizer executable located at [{self.MMV_PACKAGE_ROOT}]"
            )

        # # Load prelude configuration

        print(f"{debug_prefix} Loading prelude configuration file")

        # Build the path the prelude file should be located at
        prelude_file = f"{self.MMV_PACKAGE_ROOT}{sep}prelude.toml"

        print(
            f"{debug_prefix} Attempting to load prelude file located at [{prelude_file}], we cannot continue if this is wrong.."
        )

        # Load the prelude file
        with open(prelude_file, "r") as f:
            self.prelude = toml.loads(f.read())

        print(
            f"{debug_prefix} Loaded prelude configuration file, data: [{self.prelude}]"
        )

        # # # Logging

        # # We can now set up logging as we have where this file is located at

        # # Reset current handlers if any

        print(
            f"{debug_prefix} Resetting Python's logging logger handlers to empty list"
        )

        # Get logger and empty the list
        logger = logging.getLogger()
        logger.handlers = []

        # Handlers on logging to file and shell output, the first one if the user says to
        handlers = [logging.StreamHandler(sys.stdout)]

        # Loglevel is defined in the prelude.toml configuration
        LOG_LEVEL = {
            "critical": logging.CRITICAL,
            "debug": logging.DEBUG,
            "error": logging.ERROR,
            "info": logging.INFO,
            "warn": logging.WARN,
            "notset": logging.NOTSET,
        }.get(self.prelude["logging"]["log_level"])

        # If user chose to log to a file, add its handler..
        if self.prelude["logging"]["log_to_file"]:

            # Hard coded where the log file will be located
            # this is only valid for the last time we run this software
            self.LOG_FILE = f"{self.MMV_PACKAGE_ROOT}{sep}last_log.log"

            # Reset the log file
            with open(self.LOG_FILE, "w") as f:
                print(
                    f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]"
                )
                f.write("")

            # Verbose and append the file handler
            print(
                f"{debug_prefix} Reset log file located at [{self.LOG_FILE}]")
            handlers.append(
                logging.FileHandler(filename=self.LOG_FILE, encoding='utf-8'))

        # .. otherwise just keep the StreamHandler to stdout

        log_format = {
            "informational":
            "[%(levelname)-8s] [%(filename)-32s:%(lineno)-3d] (%(relativeCreated)-6d) %(message)s",
            "pretty":
            "[%(levelname)-8s] (%(relativeCreated)-5d)ms %(message)s",
            "economic":
            "[%(levelname)s::%(filename)s::%(lineno)d] %(message)s",
            "onlymessage": "%(message)s"
        }.get(self.prelude["logging"]["log_format"])

        # Start the logging global class, output to file and stdout
        logging.basicConfig(
            level=LOG_LEVEL,
            format=log_format,
            handlers=handlers,
        )

        # Greeter message :)
        self.greeter_message()

        # Start logging message
        bias = " " * ((self.terminal_width // 2) - 13)
        print(f"{bias[:-1]}# # [ Start Logging ] # #\n")
        print("-" * self.terminal_width + "\n")

        # Log what we'll do next
        logging.info(
            f"{debug_prefix} We're done with the pre configuration of Python's behavior and loading prelude.toml configuration file"
        )

        # Log precise Python version
        sysversion = sys.version.replace("\n", " ").replace("  ", " ")
        logging.info(f"{debug_prefix} Running on Python: [{sysversion}]")

        # # # FIXME: Python 3.9, go home you're drunk

        # Max python version, show info, assert, pretty print
        maximum_working_python_version = (3, 8)
        pversion = sys.version_info

        # Log and check
        logging.info(
            f"{debug_prefix} Checking if Python <= {maximum_working_python_version} for a working version.. "
        )

        # Huh we're on Python 2..?
        if pversion[0] == 2:
            logging.error(
                f"{debug_prefix} Please upgrade to at least Python 3")
            sys.exit(-1)

        # Python is ok
        if (pversion[0] <= maximum_working_python_version[0]) and (
                pversion[1] <= maximum_working_python_version[1]):
            logging.info(f"{debug_prefix} Ok, good python version")
        else:
            # Warn Python 3.9 is a bit unstable, even the developer had issues making it work
            logging.warn(
                f"{debug_prefix} Python 3.9 is acting a bit weird regarding some dependencies on some systems, while it should be possible to run, take it with some grain of salt and report back into the discussions troubles or workarounds you found?"
            )
            input("\n [ Press enter to continue.. ]: ")

        # # The operating system we're on, one of "linux", "windows", "macos"

        # Get the desired name from a dict matching against os.name
        if platform is None:
            self.os = {
                "posix": "linux",
                "nt": "windows",
                "darwin": "macos"
            }.get(os.name)
        else:
            logging.info(
                f"{debug_prefix} Overriding platform OS to = [{platform}]")
            self.os = platform

        # Log which OS we're running
        logging.info(
            f"{debug_prefix} Running Modular Music Visualizer on Operating System: [{self.os}]"
        )
        logging.info(f"{debug_prefix} (os.path.sep) is [{sep}]")

        # # Create interface's classes

        logging.info(f"{debug_prefix} Creating Utils() class")
        self.utils = Utils()

        logging.info(f"{debug_prefix} Creating Download() class")
        self.download = Download()

        # # Common directories between packages

        # Externals
        self.externals_dir = f"{self.MMV_PACKAGE_ROOT}{sep}externals"
        logging.info(f"{debug_prefix} Externals dir is [{self.externals_dir}]")
        self.utils.mkdir_dne(path=self.externals_dir, silent=True)

        # Downloads (inside externals)
        self.downloads_dir = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}downloads"
        logging.info(f"{debug_prefix} Downloads dir is [{self.downloads_dir}]")
        self.utils.mkdir_dne(path=self.downloads_dir, silent=True)

        # Data dir
        self.data_dir = f"{self.MMV_PACKAGE_ROOT}{sep}data"
        logging.info(f"{debug_prefix} Data dir is [{self.data_dir}]")
        self.utils.mkdir_dne(path=self.data_dir, silent=True)

        # Windoe juuuust in case
        if self.os == "windows":
            logging.info(
                f"{debug_prefix} Appending the Externals directory to system path juuuust in case..."
            )
            sys.path.append(self.externals_dir)

        # # Common files

        self.last_session_info_file = f"{self.data_dir}{sep}last_session_info.toml"
        logging.info(
            f"{debug_prefix} Last session info file is [{self.last_session_info_file}], resetting it.."
        )

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

        # # External dependencies where to append for PATH

        # Externals directory for Linux
        self.externals_dir_linux = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}linux"
        logging.info(
            f"{debug_prefix} Externals directory for Linux OS is [{self.externals_dir_linux}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_linux, silent=True)

        # Externals directory for Windows
        self.externals_dir_windows = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}windows"
        logging.info(
            f"{debug_prefix} Externals directory for Windows OS is [{self.externals_dir_windows}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_windows, silent=True)

        # Externals directory for macOS
        self.externals_dir_macos = f"{self.MMV_PACKAGE_ROOT}{sep}externals{sep}macos"
        logging.info(
            f"{debug_prefix} Externals directory for Darwin OS (macOS) is [{self.externals_dir_macos}]"
        )
        self.utils.mkdir_dne(path=self.externals_dir_macos, silent=True)

        # # This native platform externals dir
        self.externals_dir_this_platform = self.__get_platform_external_dir(
            self.os)
        logging.info(
            f"{debug_prefix} This platform externals directory is: [{self.externals_dir_this_platform}]"
        )

        # Update the externals search path (create one in this case)
        self.update_externals_search_path()

        # Code flow management
        if self.prelude["flow"]["stop_at_initialization"]:
            logging.critical(
                f"{debug_prefix} Exiting as stop_at_initialization key on prelude.toml is True"
            )
            sys.exit(0)

    # Get the target externals dir for this platform
    def __get_platform_external_dir(self, platform):
        debug_prefix = "[MMVPackageInterface.__get_platform_external_dir]"

        # # This platform externals dir
        externals_dir = {
            "linux": self.externals_dir_linux,
            "windows": self.externals_dir_windows,
            "macos": self.externals_dir_macos,
        }.get(platform)

        # log action
        logging.info(
            f"{debug_prefix} Return external dir for platform [{platform}] -> [{externals_dir}]"
        )

        return externals_dir

    # Update the self.EXTERNALS_SEARCH_PATH to every recursive subdirectory on the platform's externals dir
    def update_externals_search_path(self):
        debug_prefix = "[MMVPackageInterface.update_externals_search_path]"

        # The subdirectories on this platform externals folder
        externals_subdirs = self.utils.get_recursively_all_subdirectories(
            self.externals_dir_this_platform)

        # When using some function like Utils.get_executable_with_name, it have an argument
        # called extra_paths, add this for searching for the full externals directory.
        # Preferably use this interface methods like find_binary instead
        self.EXTERNALS_SEARCH_PATH = [self.externals_dir_this_platform]

        # If we do have subdirectories on this platform externals then append to it
        if externals_subdirs:
            self.EXTERNALS_SEARCH_PATH += externals_subdirs

    # Search for something in system's PATH, also searches for the externals folder
    # Don't append the extra .exe because Linux, macOS doesn't have these, returns False if no binary was found
    def find_binary(self, binary):
        debug_prefix = "[MMVPackageInterface.find_binary]"

        logging.info(STEP_SEPARATOR)

        # Append .exe for Windows
        if self.os == "windows":
            binary += ".exe"

        # Log action
        logging.info(
            f"{debug_prefix} Finding binary in PATH and EXTERNALS directories: [{binary}]"
        )

        return self.utils.get_executable_with_name(
            binary, extra_paths=self.EXTERNALS_SEARCH_PATH)

    # Make sure we have some target Externals, downloads latest release for them.
    # For forcing to download the Windows binaries for a release, send platform="windows" for overwriting
    # otherwise it'll be set to this class's os.
    #
    # For FFmpeg, mpv: Linux and macOS people please install from your distro's package manager.
    #
    # Possible values for target are: ["ffmpeg", "mpv", "musescore"]
    #
    def check_download_externals(self, target_externals=[], platform=None):
        debug_prefix = "[MMVPackageInterface.check_download_externals]"

        # Overwrite os if user set to a specific one
        if platform is None:
            platform = self.os
        else:
            # Error assertion, only allow linux, macos or windows target os
            valid = ["linux", "macos", "windows"]
            if not platform in valid:
                err = f"Target os [{platform}] not valid: should be one of {valid}"
                logging.error(f"{debug_prefix} {err}")
                raise RuntimeError(err)

        # Force the externals argument to be a list
        target_externals = self.utils.force_list(target_externals)

        # Log action
        logging.info(
            f"{debug_prefix} Checking externals {target_externals} for os = [{platform}]"
        )

        # We're frozen (running from release..)
        if getattr(sys, 'frozen', False):
            logging.info(
                f"{debug_prefix} Not checking for externals because is executable build.. (should have them bundled?)"
            )
            return

        # Short hand
        sep = os.path.sep

        # The target externals dir for this platform, it must be windows if we're here..
        target_externals_dir = self.__get_platform_external_dir(platform)

        # For each target external
        for external in target_externals:
            debug_prefix = "[MMVPackageInterface.check_download_externals]"
            logging.info(
                f"{debug_prefix} Checking / downloading external: [{external}] for platform [{platform}]"
            )

            # # FFmpeg / FFprobe

            if external == "ffmpeg":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(binary="ffmpeg")
                    continue

                # If we don't have FFmpeg binary on externals dir
                if not self.find_binary("ffmpeg"):

                    # Get the latest release number of ffmpeg
                    repo = "https://api.github.com/repos/BtbN/FFmpeg-Builds/releases/latest"
                    logging.info(
                        f"{debug_prefix} Getting latest release info on repository: [{repo}]"
                    )
                    ffmpeg_release = json.loads(
                        self.download.get_html_content(repo))

                    # The assets (downloadable stuff)
                    assets = ffmpeg_release["assets"]

                    logging.info(
                        f"{debug_prefix} Available assets to download (checking for non shared, gpl, non vulkan release):"
                    )

                    # Parsing the version we target and want
                    for item in assets:

                        # The name of the
                        name = item["name"]
                        logging.info(f"{debug_prefix} - [{name}]")

                        # Expected stuff
                        is_lgpl = "lgpl" in name
                        is_shared = "shared" in name
                        have_vulkan = "vulkan" in name
                        from_master = "N" in name

                        # Log what we expect
                        logging.info(
                            f"{debug_prefix} - :: Is LGPL:                   [{is_lgpl:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Is Shared:                 [{is_shared:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Have Vulkan:               [{have_vulkan:<1}] (expect: 0)"
                        )
                        logging.info(
                            f"{debug_prefix} - :: Master branch (N in name): [{from_master:<1}] (expect: 0)"
                        )

                        # We have a match!
                        if not (is_lgpl + is_shared + have_vulkan +
                                from_master):
                            logging.info(
                                f"{debug_prefix} - >> :: We have a match!!")
                            download_url = item["browser_download_url"]
                            break

                    logging.info(
                        f"{debug_prefix} Download URL: [{download_url}]")

                    # Where we'll save the compressed zip of FFmpeg
                    ffmpeg_zip = self.downloads_dir + f"{sep}{name}"

                    # Download FFmpeg build
                    self.download.wget(download_url, ffmpeg_zip,
                                       f"FFmpeg v={name}")

                    # Extract the files
                    self.download.extract_zip(ffmpeg_zip, target_externals_dir)

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [ffmpeg] binary in externals / system path!!"
                    )

            # # MPV

            if external == "mpv":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(
                        binary="mpv",
                        help_fix=f"Visit [https://mpv.io/installation/]")
                    continue

                # If we don't have mpv binary on externals dir or system's path
                if not self.find_binary("mpv"):

                    mpv_7z_version = "mpv-x86_64-20201220-git-dde0189.7z"

                    # Where we'll save the compressed zip of FFmpeg
                    mpv_7z = self.downloads_dir + f"{sep}{mpv_7z_version}"

                    # Download mpv build
                    self.download.wget(
                        f"https://sourceforge.net/projects/mpv-player-windows/files/64bit/{mpv_7z_version}/download",
                        mpv_7z, f"MPV v=20201220-git-dde0189")

                    # Where to extract final mpv
                    mpv_extracted_folder = f"{self.externals_dir_this_platform}{sep}" + mpv_7z_version.replace(
                        ".7z", "")
                    self.utils.mkdir_dne(path=mpv_extracted_folder)

                    # Extract the files
                    self.download.extract_file(mpv_7z, mpv_extracted_folder)

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [mpv] binary in externals / system path!!"
                    )

            # # MPV

            if external == "musescore":
                debug_prefix = f"[MMVPackageInterface.check_download_externals({external})]"

                # We're on Linux / macOS so checking ffmpeg external dependency on system's path
                if platform in ["linux", "macos"]:
                    self.__cant_micro_manage_external_for_you(
                        binary="musescore",
                        help_fix=
                        f"Go to [https://musescore.org/en/download] and install for your platform"
                    )
                    continue

                # If we don't have musescore binary on externals dir or system's path
                if not self.find_binary("musescore"):

                    musescore_version = "v3.5.2/MuseScorePortable-3.5.2.311459983-x86.paf.exe"

                    # Download musescore
                    self.download.wget(
                        f"https://cdn.jsdelivr.net/musescore/{musescore_version}",
                        f"{self.externals_dir_this_platform}{sep}musescore.exe",
                        f"Musescore Portable v=[{musescore_version}]")

                else:  # Already have the binary
                    logging.info(
                        f"{debug_prefix} Already have [musescore] binary in externals / system path!!"
                    )

            # Update the externals search path because we downloaded stuff
            self.update_externals_search_path()

        logging.info(STEP_SEPARATOR)

    # Ensure we have an external dependency we can't micro manage because too much entropy
    def __cant_micro_manage_external_for_you(self, binary, help_fix=None):
        debug_prefix = "[MMVPackageInterface.__cant_micro_manage_external_for_you]"

        logging.info(
            f"{debug_prefix} You are using Linux or macOS, please make sure you have [{binary}] package binary installed on your distro or on homebrew, we'll just check for it nowm, can't continue if you don't have it.."
        )

        # Can't continue
        if not self.find_binary(binary):
            logging.error(
                f"{debug_prefix} Couldn't find lowercase [{binary}] binary on PATH, install from your Linux distro package manager / macOS homebrew, please install it"
            )

            # Log any extra help we give the user
            if help_fix is not None:
                logging.error(f"{debug_prefix} {help_fix}")

            sys.exit(-1)
示例#26
0
class mmv:

    # Start default configs, creates wrapper classes
    def __init__(self, watch_processing_video_realtime: bool = False) -> None:

        # Main class of MMV
        self.mmv = MMVMain()

        # Utilities
        self.utils = Utils()

        # Start MMV classes that main connects them, do not run
        self.mmv.setup()

        # Default options of performance and quality, 720p60
        self.quality()

        # Configuring options
        self.quality_preset = QualityPreset(self)
        self.audio_processing = AudioProcessingPresets(self)
        self.post_processing = self.mmv.canvas.configure

        # Has the user chosen to watch the processing video realtime?
        self.mmv.context.watch_processing_video_realtime = watch_processing_video_realtime

    # Execute MMV with the configurations we've done
    def run(self) -> None:
        self.mmv.run()

    # Define output video width, height and frames per second
    def quality(self,
                width: int = 1280,
                height: int = 720,
                fps: int = 60,
                batch_size=2048) -> None:
        self.mmv.context.width = width
        self.mmv.context.height = height
        self.mmv.context.fps = fps
        self.mmv.context.batch_size = batch_size
        self.width = width
        self.height = height
        self.resolution = [width, height]
        self.mmv.canvas.create_canvas()

    def set_path(self, path, message="path"):
        path = self.utils.get_abspath(path)
        if not os.path.exists(path):
            raise FileNotFoundError(f"Input {message} does not exist {path}")
        return path

    # Set the input audio file, raise exception if it does not exist
    def input_audio(self, path: str) -> None:
        self.mmv.context.input_file = self.set_path(path)

    # Set the input audio file, raise exception if it does not exist
    def input_midi(self, path: str) -> None:
        self.mmv.context.input_midi = self.set_path(path)

    # Output path where we'll be saving the final video
    def output_video(self, path: str) -> None:
        path = self.utils.get_abspath(path)
        self.mmv.context.output_video = path

    def offset_audio_steps(self, steps=0):
        self.mmv.context.offset_audio_before_in_many_steps = steps

    # Set the assets dir
    def set_assets_dir(self, path: str) -> None:
        # Remove the last "/"", pathing intuition under MMV scripts gets easier
        if path.endswith("/"):
            path = path[:-1]
        path = self.utils.get_abspath(path)
        self.utils.mkdir_dne(path)
        self.assets_dir = path
        self.mmv.context.assets = path

    # # [ MMV Objects ] # #

    # Add a given object to MMVAnimation content on a given layer
    def add(self, item, layer: int = 0) -> None:

        # Make layers until this given layer if they don't exist
        self.mmv.mmv_animation.mklayers_until(layer)

        # Check the type and add accordingly
        if self.utils.is_matching_type([item], [MMVImage]):
            self.mmv.mmv_animation.content[layer].append(item)

        if self.utils.is_matching_type([item], [MMVGenerator]):
            self.mmv.mmv_animation.generators.append(item)

    # Get a blank MMVImage object
    def image_object(self) -> None:
        return MMVImage(self.mmv)

    # Get a pygradienter object with many workers for rendering
    def pygradienter(self, workers=4):
        return pygradienter(workers=workers)

    # Get a blank MMVGenerator object
    def generator_object(self):
        return MMVGenerator(self.mmv)

    # # [ Utilities ] # #

    def random_file_from_dir(self, path):
        return self.utils.random_file_from_dir(path)

    def get_unique_id(self):
        return self.utils.get_hash(str(uuid.uuid4()))

    # # [ APPS ] # #

    def pyskt_test(self, *args, **kwargs):
        print(args, kwargs)
        return PysktMain(self.mmv, *args, **kwargs)
示例#27
0
class MMVShaderMaker:
    def __init__(self, working_directory, name = None, replaces = {}):
        debug_prefix = "[MMVShaderMaker.__init__]"

        # Instantiate classes
        self.utils = Utils()
        self.block_of_code = BlockOfCode

        # Refactors
        self.transformations = MMVShaderMakerTransformations()
        self.loaders = MMVShaderMakerLoaders(mmv_shader_maker = self)
        self.macros = MMVShaderMakerMacros(mmv_shader_maker = self)
    
        # Where to place directories and whatnot
        self.working_directory = working_directory

        # # Sessions and Shader Maker runtime directory

        # Reset runtime directory
        logging.info(f"{debug_prefix} Resetting directory")
        self.utils.reset_dir(self.working_directory)

        # # Add stuff

        # Attributes
        self._includes = []
        self._mappings = []
        self._functions = []
        self._transformations = []

        # Get and add name mapping
        self.name = name
        self.replaces = replaces
     
        # Start with the base shader
        self._fragment_shader = BASE_FRAGMENT_SHADER
        self._final_shader = None
        self._path_on_disk = None

    def clone(self): return copy.deepcopy(self)
    def set_name(self, name): self.name = name

    # Build the shader and save to the working directory, returns the final path
    # Append suffix is useful for layer shaders, set to False for includes
    def finish(self, append_prefix = True):
        debug_prefix = "[MMVShaderMaker.finish]"
        if self.name is None:
            self.name = str(uuid.uuid4())
        elif append_prefix:
            self.name = f"[{len(os.listdir(self.working_directory))}]_{self.name}"
        save = self.working_directory / f"{self.name}.glsl"
        logging.info(f"{debug_prefix} Finishing shader [{self.name}] saving to [{save}]")
        self.build_final_shader()
        self.save_shader_to_file(save)
        return self.get_path()

    # Load full shader from path
    def load_shader_from_path(self, path: Path, replaces = {}, get_name_from_file = True):
        debug_prefix = "[MMVShaderMaker.load_from_path]"
        path = self.utils.enforce_pathlib_Path(path)
        
        # Concatenate both dictionaries
        for key, item in self.replaces.items():
            replaces[key] = item

        # Log action
        logging.info(f"{debug_prefix} Loading shader from path [{path}]")
        logging.info(f"{debug_prefix} Replaces: {replaces}")

        # Assign same name of the file
        if (self.name is None) or get_name_from_file:
            logging.info(f"{debug_prefix} No name so far, getting filename.. will error if not pathlib.Path")
            self.name = path.stem
            logging.info(f"{debug_prefix} Ok! Name is [{self.name}]")

        # Load file on path
        with open(path, "r") as f:
            data = f.read()

        # Replace stuff
        for key, value in replaces.items():
            logging.info(f"{debug_prefix} | Replacing [{key}] -> [{value}]")
            data = data.replace(f"{{{key}}}", f"{value}")
        
        # Assign data to fragment shader
        self._fragment_shader = data

    # # # # Add functions

    # # Mappings

    # Return a pretty dictionary and commented with //, multi lines like:
    # //#mmv {                                                                                                                         //  //
    # //  'anisotropy': 16,                                                                                                             //  //
    # //  'height': None,    
    # ...
    # //}
    def __pretty_mapper(self, dictionary):
        pretty = pprint.pformat(dictionary)[1:-1].split("\n")

        # Add newlines on {}
        pretty.insert(0, "{")
        pretty.append("//}")

        # Iterate on lines
        processing = []
        for index, line in enumerate(pretty):

            # Add coment if not start or end (because we put //#mmv)
            if not ( (index == 0) or (index == len(pretty) - 1) ):
                line = f"// {line}"

          # Append and join
            processing.append(line)
        return '\n'.join(processing)

    # Add image from file mapping to a target width and height.
    # Uses the resolution of the read file if some width or height is None (or both)
    def add_image_mapping(self, name, path, width = None, height = None, repeat_x = True, repeat_y = True, mipmaps = True, anisotropy = 16):
        debug_prefix = "[MMVShaderMaker.add_image_mapping]"
        path = self.utils.enforce_pathlib_Path(path)
        mapping = {"type": "map", "name": name, "loader": "image", "value": str(path), "width": width, "height": height, "mipmaps": mipmaps, "repeat_x": repeat_x, "repeat_y": repeat_y, "anisotropy": anisotropy}
        mapping = self.__pretty_mapper(dictionary = mapping)
        self._mappings.append(BlockOfCode(f"//#mmv {mapping}", scoped = False, name = f"{debug_prefix}"))

    # Video mapping to a target width and height
    def add_video_mapping(self, name, path, width, height, anisotropy = 16):
        debug_prefix = "[MMVShaderMaker.add_video_mapping]"
        path = self.utils.enforce_pathlib_Path(path)
        mapping = {"type": "map", "name": name, "loader": "video", "value": str(path), "width": width, "height": height, "anisotropy": anisotropy}
        mapping = self.__pretty_mapper(dictionary = mapping)
        self._mappings.append(BlockOfCode(f"//#mmv {mapping}", scoped = False, name = f"{debug_prefix}"))

    # Pipeline (as) texture is for communicating big arrays from Python to ModernGL's shader being executed
    def add_pipeline_texture_mapping(self, name, width, height, depth, repeat_x = False, repeat_y = False, mipmaps = True, anisotropy = 16):
        debug_prefix = "[MMVShaderMaker.add_pipeline_texture_mapping]"
        mapping = {"type": "map", "name": name, "loader": "pipeline_texture", "width": width, "height": height, "depth": depth, "repeat_x": repeat_x, "repeat_y": repeat_y, "mipmaps": mipmaps, "anisotropy": anisotropy}
        mapping = self.__pretty_mapper(dictionary = mapping)
        self._mappings.append(BlockOfCode(f"//#mmv {mapping}", scoped = False, name = f"{debug_prefix}"))

    # Strict shader only renders at this target resolution internally, regardless of output dimensions
    def add_strict_shader_mapping(self, name, path, width, height, anisotropy = 16):
        debug_prefix = "[MMVShaderMaker.add_strict_shader_mapping]"
        path = self.utils.enforce_pathlib_Path(path)
        mapping = {"type": "map", "name": name, "loader": "shader", "value": str(path), "width": width, "height": height, "anisotropy": anisotropy}
        mapping = self.__pretty_mapper(dictionary = mapping)
        self._mappings.append(BlockOfCode(f"//#mmv {mapping}", scoped = False, name = f"{debug_prefix}"))

    # Dynamic shader adapts to the viewport / output dimensions, recommended
    def add_dynamic_shader_mapping(self, name, path, anisotropy = 16):
        debug_prefix = "[MMVShaderMaker.add_dynamic_shader_mapping]"
        path = self.utils.enforce_pathlib_Path(path)
        mapping = {"type": "map", "name": name, "loader": "dynshader", "value": str(path), "anisotropy": anisotropy}
        mapping = self.__pretty_mapper(dictionary = mapping)
        self._mappings.append(BlockOfCode(f"//#mmv {mapping}", scoped = False, name = f"{debug_prefix}"))

    # Name the shader on this class's name
    def _add_name_mapping(self):
        debug_prefix = "[MMVShaderMaker.add_name_mapping]"
        mapping = {"type": "name", "value": self.name}
        mapping = self.__pretty_mapper(dictionary = mapping)
        self._mappings.append(BlockOfCode(f"//#mmv {mapping}", scoped = False, name = f"{debug_prefix}"))

    # # Functions

    # Append some function to this shader
    def add_function(self, function: BlockOfCode):
        debug_prefix = "[MMVShaderMaker.add_include]"
        function.scoped = False  # Enforce non scoped functions
        self._functions.append(function)

    # # Includes

    # Include some file
    def add_include(self, include: str, mode = "multiple"):
        debug_prefix = "[MMVShaderMaker.add_include]"
        mapping = {"type": "include", "value": include, "mode": mode}
        self._includes.append(BlockOfCode(f"//#mmv {mapping}", scoped = False, name = f"{debug_prefix}"))

    # # Transformations

    # Append some transformation to this shader (executed in main function)
    def add_transformation(self, transformation: BlockOfCode):
        debug_prefix = "[MMVShaderMaker.add_transformation]"
        self._transformations.append(transformation)

    # # # # Generating, saving, getting strings

    def __replace_progressive(self, marker: str, data: str, indent: str):
        debug_prefix = "[MMVShaderMaker.__replace_progressive]"
        self._fragment_shader = self._fragment_shader.replace(f"{indent}{marker}", f"{data}{indent}{marker}")

    # # Core loop

    # Build the final shader, assign it to self._final_shader
    def build_final_shader(self) -> BlockOfCode:
        debug_prefix = "[MMVShaderMaker.build_final_shader]"
        self._add_name_mapping()

        # Replaces pair of name on the final shader and the items
        replaces = [
            ["includes", self._includes],
            ["mappings", self._mappings],
            ["functions", self._functions],
            ["transformations", self._transformations],
        ]

        # For each pair, replace with that object's content
        for mapping_type, items in replaces:

            # The marker we search for
            marker = f"//#shadermaker {mapping_type}"

            for block_of_code in items:
                have_marker = False
                for line in self._fragment_shader.split("\n"):
                    if marker in line:
                        indent = line.split(marker)[0]
                        have_marker = True

                if have_marker:
                    self.__replace_progressive(
                        marker = marker,
                        data = block_of_code.get_string_content(indent = indent),
                        indent = indent,
                    )
        
        # Assign final shader
        self._final_shader = BlockOfCode(self._fragment_shader, scoped = False, name = self.name)

    # String of the final shader
    def get_final_shader_string(self) -> str:
        debug_prefix = "[MMVShaderMaker.get_final_shader_string]"

        # Can't get shader if didn't run .build_final_shader()
        assert self._final_shader is not None, "You haven't run .build_final_shader()"
        return self._final_shader.get_string_content()

    # Save this shader to a file
    def save_shader_to_file(self, path):
        debug_prefix = "[MMVShaderMaker.save_shader_to_file]"
        self._path_on_disk = path

        # Log action
        logging.info(f"{debug_prefix} Saving shader name [{self.name}] to path [{path}]")

        # Open the file on the path and write the strings
        with open(path, "w") as shader_file:
            shader_file.write(self.get_final_shader_string())

    # Get the shader's path    
    def get_path(self):
        assert self._path_on_disk is not None, "You haven't run .save_shader_to_file()"
        return self._path_on_disk
示例#28
0
class MMVShaderMakerTransformations:
    def __init__(self):
        self.utils = Utils()
    
    # Blit some image at some x, y at certain scale, angle, etc.
    def image(self,
        image, assign_to_variable = "layered", new_variable = True,
        x = 0, y = 0, scale = 1.0, angle = 0,
        canvas = "layered", uv = "stuv", anchor = "vec2(0.5, 0.5)",
        shift = "vec2(0.5, 0.5)", repeat = False,
        undo_gamma = True, gamma = 2.0, **kwargs
    ):
        debug_prefix = "[MMVShaderMakerTransformations.image]"

        new = "vec4 " if new_variable else ""
        repeat = self.utils.bool_to_string(repeat)
        undo_gamma = self.utils.bool_to_string(undo_gamma)

        boc = BlockOfCode((
            f"{new}{assign_to_variable} = mmv_blit_image(\n"
            f"    {canvas}, // Canvas\n"
            f"    {image}, // Image\n"
            f"    {image}_resolution, // Resolution\n"
            f"    {uv}, // UV\n"
            f"    {anchor}, // Anchor\n"
            f"    {shift}, // Shift\n"
            f"    {scale}, // Scale\n"
            f"    {angle}, // Angle\n"
            f"    {repeat}, // Repeat\n"
            f"    {undo_gamma}, // Undo gamma\n"
            f"    {gamma} // Gamma\n"
            f")"
        ), scoped = kwargs.get("scoped", True))

        logging.info(f"{debug_prefix} Returning BlockOfCode:")
        for line in boc.get_content(): logging.info(f"{debug_prefix} | {line}")
        return boc

    def get_texture(self, texture_name, uv = "stuv", assign_to_variable = "processing", new_variable = True, **kwargs):
        debug_prefix = "[MMVShaderMakerTransformations.get_texture]"

        new = "vec4 " if new_variable else ""
        boc = BlockOfCode((
            f"{new}{assign_to_variable} = texture({texture_name}, {uv});"
        ), scoped = kwargs.get("scoped", True))
        logging.info(f"{debug_prefix} Returning BlockOfCode:")
        for line in boc.get_content(newline = False): logging.info(f"{debug_prefix} | {line}")
        return boc

    # Alpha
    def alpha_composite(self, new, old = "layered", **kwargs):
        debug_prefix = "[MMVShaderMakerTransformations.alpha_composite]"

        boc = BlockOfCode((
            f"layered = mmv_alpha_composite({new}, {old});"
        ), scoped = kwargs.get("scoped", False))
        logging.info(f"{debug_prefix} Returning BlockOfCode:")
        for line in boc.get_content(newline = False): logging.info(f"{debug_prefix} | {line}")
        return boc
    
    def gamma_correction(self, exponent = 2.0, **kwargs):
        debug_prefix = "[MMVShaderMakerTransformations.gamma_coorection]"

        boc = BlockOfCode((
            f"layered = pow(layered, vec4(1.0 / {exponent}));"
        ), scoped = kwargs.get("scoped", False))
        logging.info(f"{debug_prefix} Returning BlockOfCode:")
        for line in boc.get_content(newline = False): logging.info(f"{debug_prefix} | {line}")
        return boc
    
    def fade_in(self, formula = "(atan(mmv_time*mmv_time)*2) / 3.141596", stop_after = 10):
        debug_prefix = "[MMVShaderMakerTransformations.fade_in]"
        boc = BlockOfCode((
            f"if (mmv_time < {stop_after}) {{\n"
            f"    float fadein = {formula};\n"
            f"    layered = fadein * layered;\n"
            f"}}"
        ), scoped = True)
        logging.info(f"{debug_prefix} Returning BlockOfCode:")
        for line in boc.get_content(newline = False): logging.info(f"{debug_prefix} | {line}")
        return boc