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
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)