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
Esempio n. 2
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)
Esempio n. 3
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)
Esempio n. 4
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)