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