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
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"
            )
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
Beispiel #4
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)