diff --git a/__init__.py b/__init__.py index fb839e4..d713572 100644 --- a/__init__.py +++ b/__init__.py @@ -1,19 +1,22 @@ # MBM's Music Visualizer # Imports -from mbmMusicVisualizer import MusicVisualizer +from .mbmMusicVisualizer import MusicVisualizer +from .mbmAudioLoader import AudioLoader # A dictionary that contains all nodes you want to export with their names # NOTE: names should be globally unique NODE_CLASS_MAPPINGS = { # "id": Class - "mbmMusicVisuzalizer": MusicVisualizer + "mbmMusicVisuzalizer": MusicVisualizer, + "mbmAudioLoader": AudioLoader } # A dictionary that contains the friendly/humanly readable titles for the nodes NODE_DISPLAY_NAME_MAPPINGS = { # "id": "readable" - "mbmMusicVisuzalizer": "Music Visualizer" + "mbmMusicVisuzalizer": "Music Visualizer", + "mbmAudioLoader": "Audio Loader" } # Export diff --git a/mbmAudioLoader.py b/mbmAudioLoader.py index 7c190bb..4aed5ca 100644 --- a/mbmAudioLoader.py +++ b/mbmAudioLoader.py @@ -3,10 +3,12 @@ # Imports import os -import mbmMVShared as mvs +import librosa + +from .mbmMVShared import fullpath, audioInputDir # Classes -class MusicVisualizer: +class AudioLoader: """ Load an audio file. """ @@ -15,17 +17,22 @@ class MusicVisualizer: @classmethod def INPUT_TYPES(s): - inputDir = mvs.audioInputDir() + inputDir = audioInputDir() localFiles = [f for f in os.listdir(inputDir) if os.path.isfile(os.path.join(inputDir, f))] + return { "required": { - "file": (sorted(localFiles), {"image_upload": True}), + "file": (sorted(localFiles),) # TODO: add more params? } } - RETURN_TYPES = ("AUDIO", ) + RETURN_TYPES = ("AUDIO", "STRING") + RETURN_NAMES = ("AUDIO", "FILENAME") FUNCTION = "process" - CATEGORY = "MBMnodes" + CATEGORY = "MBMnodes/MusicVisualizer" - def process(self): - return None \ No newline at end of file + def process(self, filepath: str): + return ( + librosa.load(fullpath(filepath)), + os.path.splitext(os.path.basename(filepath))[0] + ) \ No newline at end of file diff --git a/mbmMusicVisualizer.py b/mbmMusicVisualizer.py index 64896b0..5277dbc 100644 --- a/mbmMusicVisualizer.py +++ b/mbmMusicVisualizer.py @@ -2,7 +2,8 @@ # Visualize a provided audio file. # Imports -# etc +import librosa +import numpy as np # Classes class MusicVisualizer: @@ -18,7 +19,7 @@ class MusicVisualizer: def INPUT_TYPES(s): return { "required": { - "audio": "AUDIO", + "audio": ("AUDIO",), "width": ("INT", {"default": 512}), "height": ("INT", {"default": 512}), "pitch": ("INT", {"default": 220}), # sensitivity @@ -27,23 +28,62 @@ class MusicVisualizer: "jitter": ("FLOAT", {"default": 0.5}), "truncation": ("FLOAT", {"default": 1.0}), "smoothing": ("INT", {"default": 20}), # factor + "hopLength": ("INT", {"default": 512}), } } RETURN_TYPES = ("IMAGE", ) RETURN_NAMES = ("IMAGES", ) FUNCTION = "process" - CATEGORY = "MBMnodes" + CATEGORY = "MBMnodes/MusicVisualizer" def process(self, - audio, + audio: tuple, width: int, height: int, - pitch: int, - tempo: int, + pitchSensitivity: int, + tempoSensitivity: int, depth: float, jitter: float, truncation: float, - smoothing: int + smoothFactor: int, + hopLen: int ): - return None \ No newline at end of file + # Unpack the audio + y, sr = audio + + # Calculate parameters + pitchSensitivity = (300 - pitchSensitivity) * 512 / hopLen + tempoSensitivity = tempoSensitivity * hopLen / 512 + + if smoothFactor > 1: + smoothFactor = int(smoothFactor * 512 / hopLen) + + # Calculate the spectrogram + spectro = librosa.feature.melspectrogram( + y=y, + sr=sr, + n_mels=128, + fmax=8000, + hop_length=hopLen + ) + + # Calculate mean power for hops + spectroMean = np.mean(spectro, axis=0) + + # Calculate power gradient for hops + spectroGrad = np.gradient(spectroMean) + spectroGrad = (spectroGrad / np.max(spectroGrad)).clip(min=0) + + # Normalize mean power + spectroMean = (spectroMean / np.min(spectroMean)) / np.ptp(spectroMean) + + # Calculate pitch chroma for hops + chroma = librosa.feature.chroma_cqt( + y=y, + sr=sr, + hop_length=hopLen + ) + + # Sort pitch chroma + chromaSort = np.argsort(np.mean(chroma, axis=1))[::-1]